Release 2.35 (#5470)

* open up API for plugins by removing internal
* use constants in entity column definition
* simplified entity management API
* bump packages
* allow installing assets and run database migrations independently
* bump to apidoc-bundle 5
* do not duplicate http method in API operationId
* new security entries in Open API definition
* changed API UI provider for Swagger to Stoplight, improved endpoint titles, hide internal endpoints
* deactivate swagger json endpoint
This commit is contained in:
Kevin Papst
2025-05-24 14:28:39 +02:00
committed by GitHub
parent 6e26a37c4d
commit 1e0fbf0b73
63 changed files with 518 additions and 529 deletions

View File

@@ -38,8 +38,6 @@ final class ActionsController extends BaseApiController
}
/**
* @param PageActionsEvent $event
* @param string $locale
* @return array<PageAction>
*/
private function convertEvent(PageActionsEvent $event, string $locale): array
@@ -65,12 +63,13 @@ final class ActionsController extends BaseApiController
}
/**
* Get all item actions for the given Timesheet [for internal use]
* Fetch item actions for Timesheet
*/
#[OA\Response(response: 200, description: 'Returns item actions for the timesheet', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to fetch', required: true)]
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], path: '/timesheet/{id}/{view}/{locale}', name: 'get_timesheet_actions', requirements: ['id' => '\d+'])]
public function getTimesheetActions(Timesheet $timesheet, string $view, string $locale): Response
{
@@ -83,12 +82,13 @@ final class ActionsController extends BaseApiController
}
/**
* Get all item actions for the given Activity [for internal use]
* Fetch item actions for Activity
*/
#[OA\Response(response: 200, description: 'Returns item actions for the activity', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to fetch', required: true)]
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], path: '/activity/{id}/{view}/{locale}', name: 'get_activity_actions', requirements: ['id' => '\d+'])]
public function getActivityActions(Activity $activity, string $view, string $locale): Response
{
@@ -101,12 +101,13 @@ final class ActionsController extends BaseApiController
}
/**
* Get all item actions for the given Project [for internal use]
* Fetch item actions for Project
*/
#[OA\Response(response: 200, description: 'Returns item actions for the project', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Project ID to fetch', required: true)]
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], path: '/project/{id}/{view}/{locale}', name: 'get_project_actions', requirements: ['id' => '\d+'])]
public function getProjectActions(Project $project, string $view, string $locale): Response
{
@@ -119,12 +120,13 @@ final class ActionsController extends BaseApiController
}
/**
* Get all item actions for the given Customer [for internal use]
* Fetch item actions for Customer
*/
#[OA\Response(response: 200, description: 'Returns item actions for the customer', content: new OA\JsonContent(ref: new Model(type: PageAction::class)))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Customer ID to fetch', required: true)]
#[OA\Parameter(name: 'view', in: 'path', description: 'View to display the actions at (e.g. index, custom)', required: true)]
#[OA\Parameter(name: 'locale', in: 'path', description: 'Language to translate the action title to (e.g. de, en)', required: true)]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], path: '/customer/{id}/{view}/{locale}', name: 'get_customer_actions', requirements: ['id' => '\d+'])]
public function getCustomerActions(Customer $customer, string $view, string $locale): Response
{

View File

@@ -53,7 +53,7 @@ final class ActivityController extends BaseApiController
}
/**
* Returns a collection of activities (which are visible to the user)
* Fetch collection of activities
*/
#[OA\Response(response: 200, description: 'Returns a collection of activities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityCollection')))]
#[Route(methods: ['GET'], path: '', name: 'get_activities')]
@@ -122,7 +122,7 @@ final class ActivityController extends BaseApiController
}
/**
* Returns one activity
* Fetch activity
*/
#[OA\Response(response: 200, description: 'Returns one activity entity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Activity ID to fetch', required: true)]
@@ -137,7 +137,7 @@ final class ActivityController extends BaseApiController
}
/**
* Creates a new activity
* Create activity
*/
#[OA\Post(description: 'Creates a new activity and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created activity', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ActivityEditForm'))]
@@ -231,7 +231,7 @@ final class ActivityController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing activity
* Update activity custom-field
*/
#[IsGranted('edit', 'activity')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ActivityEntity'))]
@@ -262,7 +262,7 @@ final class ActivityController extends BaseApiController
}
/**
* Returns a collection of all rates for one activity
* Fetch all rates for one activity
*/
#[IsGranted('edit', 'activity')]
#[OA\Response(response: 200, description: 'Returns a collection of activity rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ActivityRate')))]
@@ -279,7 +279,7 @@ final class ActivityController extends BaseApiController
}
/**
* Deletes one rate for an activity
* Delete rate for activity
*/
#[IsGranted('edit', 'activity')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
@@ -300,7 +300,7 @@ final class ActivityController extends BaseApiController
}
/**
* Adds a new rate to an activity
* Add rate for one activity
*/
#[IsGranted('edit', 'activity')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ActivityRate'))])]

View File

@@ -28,7 +28,7 @@ final class ConfigurationController extends BaseApiController
}
/**
* Returns the timesheet configuration
* Fetch timesheet configuration
*/
#[OA\Response(response: 200, description: 'Returns the instance specific timesheet configuration', content: new OA\JsonContent(ref: new Model(type: TimesheetConfig::class)))]
#[Route(path: '/config/timesheet', methods: ['GET'])]
@@ -48,7 +48,7 @@ final class ConfigurationController extends BaseApiController
}
/**
* Returns the configured color codes and names
* Fetch configured color codes
*/
#[OA\Response(response: 200, description: 'Returns the configured color codes and names', content: new OA\JsonContent(type: 'object', example: ['Red' => '#ff0000'], additionalProperties: new OA\AdditionalProperties(type: 'string')))]
#[Route(path: '/config/colors', methods: ['GET'])]

View File

@@ -52,7 +52,7 @@ final class CustomerController extends BaseApiController
}
/**
* Returns a collection of customers (which are visible to the user)
* Fetch collection of customers
*/
#[OA\Response(response: 200, description: 'Returns a collection of customers', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerCollection')))]
#[Route(methods: ['GET'], path: '', name: 'get_customers')]
@@ -98,7 +98,7 @@ final class CustomerController extends BaseApiController
}
/**
* Returns one customer
* Fetch customer
*/
#[OA\Response(response: 200, description: 'Returns one customer entity', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))]
#[Route(methods: ['GET'], path: '/{id}', name: 'get_customer', requirements: ['id' => '\d+'])]
@@ -112,7 +112,7 @@ final class CustomerController extends BaseApiController
}
/**
* Creates a new customer
* Create customer
*/
#[OA\Post(description: 'Creates a new customer and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created customer', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CustomerEditForm'))]
@@ -206,7 +206,7 @@ final class CustomerController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing customer
* Update customer custom-field
*/
#[IsGranted('edit', 'customer')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/CustomerEntity'))]
@@ -237,7 +237,7 @@ final class CustomerController extends BaseApiController
}
/**
* Returns a collection of all rates for one customer
* Fetch all rates for one customer
*/
#[IsGranted('edit', 'customer')]
#[OA\Response(response: 200, description: 'Returns a collection of customer rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/CustomerRate')))]
@@ -254,7 +254,7 @@ final class CustomerController extends BaseApiController
}
/**
* Deletes one rate for a customer
* Delete rate for customer
*/
#[IsGranted('edit', 'customer')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
@@ -275,7 +275,7 @@ final class CustomerController extends BaseApiController
}
/**
* Adds a new rate to a customer
* Add rate for one customer
*/
#[IsGranted('edit', 'customer')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/CustomerRate'))])]

View File

@@ -38,9 +38,7 @@ final class InvoiceController extends BaseApiController
}
/**
* Returns a paginated collection of invoices.
*
* Needs permission: view_invoice
* Fetch collection of invoices
*/
#[IsGranted('view_invoice')]
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
@@ -89,9 +87,7 @@ final class InvoiceController extends BaseApiController
}
/**
* Returns one invoice.
*
* Needs permission: view_invoice
* Fetch invoice
*/
#[IsGranted('view_invoice')]
#[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))]

View File

@@ -54,7 +54,7 @@ final class ProjectController extends BaseApiController
}
/**
* Returns a collection of projects (which are visible to the user)
* Fetch collection of projects
*/
#[OA\Response(response: 200, description: 'Returns a collection of projects', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectCollection')))]
#[Route(methods: ['GET'], path: '', name: 'get_projects')]
@@ -165,7 +165,7 @@ final class ProjectController extends BaseApiController
}
/**
* Creates a new project
* Create project
*/
#[OA\Post(description: 'Creates a new project and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created project', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/ProjectEditForm'))]
@@ -188,7 +188,7 @@ final class ProjectController extends BaseApiController
$form->submit($request->request->all());
if ($form->isValid()) {
$this->projectService->saveNewProject($project);
$this->projectService->saveProject($project);
$view = new View($project, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -232,7 +232,7 @@ final class ProjectController extends BaseApiController
return $this->viewHandler->handle($view);
}
$this->projectService->updateProject($project);
$this->projectService->saveProject($project);
$view = new View($project, Response::HTTP_OK);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -260,7 +260,7 @@ final class ProjectController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing project
* Update project custom-field
*/
#[IsGranted('edit', 'project')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/ProjectEntity'))]
@@ -282,7 +282,7 @@ final class ProjectController extends BaseApiController
$meta->setValue($value);
$this->projectService->updateProject($project);
$this->projectService->saveProject($project);
$view = new View($project, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -291,7 +291,7 @@ final class ProjectController extends BaseApiController
}
/**
* Returns a collection of all rates for one project
* Fetch all rates for one project
*/
#[IsGranted('edit', 'project')]
#[OA\Response(response: 200, description: 'Returns a collection of project rate entities', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/ProjectRate')))]
@@ -308,7 +308,7 @@ final class ProjectController extends BaseApiController
}
/**
* Deletes one rate for a project
* Delete rate for project
*/
#[IsGranted('edit', 'project')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
@@ -329,7 +329,7 @@ final class ProjectController extends BaseApiController
}
/**
* Adds a new rate to a project
* Add rate for one project
*/
#[IsGranted('edit', 'project')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the new created rate', content: new OA\JsonContent(ref: '#/components/schemas/ProjectRate'))])]

View File

@@ -29,7 +29,7 @@ final class StatusController extends BaseApiController
}
/**
* A testing route for the API
* Testing route for the API
*/
#[OA\Response(response: 200, description: "A simple route that returns a 'pong', which you can use for testing the API", content: new OA\JsonContent(example: "{'message': 'pong'}"))]
#[Route(methods: ['GET'], path: '/ping')]
@@ -41,7 +41,7 @@ final class StatusController extends BaseApiController
}
/**
* Returns information about the Kimai release
* Fetch Kimai release
*/
#[OA\Response(response: 200, description: 'Returns version information about the current release', content: new OA\JsonContent(ref: new Model(type: Version::class)))]
#[Route(methods: ['GET'], path: '/version')]
@@ -51,7 +51,7 @@ final class StatusController extends BaseApiController
}
/**
* Returns information about installed Plugins
* Fetch installed Plugins
*/
#[OA\Response(response: 200, description: 'Returns a list of plugin names and versions', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: new Model(type: Plugin::class))))]
#[Route(methods: ['GET'], path: '/plugins')]

View File

@@ -39,9 +39,10 @@ final class TagController extends BaseApiController
}
/**
* Deprecated: Fetch tags by filter as string collection
* Deprecated: Fetch tags as strings
*/
#[OA\Response(response: 200, description: 'Returns the collection of all existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))]
#[OA\Response(response: 200, description: 'DEPRECATED: Returns existing tags as string array', content: new OA\JsonContent(type: 'array', items: new OA\Items(type: 'string')))]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], name: 'get_tags')]
#[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')]
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
@@ -58,7 +59,7 @@ final class TagController extends BaseApiController
}
/**
* Fetch tags by filter (as full entities)
* Fetch collection of tags
*/
#[OA\Response(response: 200, description: 'Find the collection of all matching tags', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TagEntity')))]
#[Route(path: '/find', name: 'get_tags_full', methods: ['GET'])]
@@ -79,7 +80,7 @@ final class TagController extends BaseApiController
}
/**
* Creates a new tag
* Create tag
*/
#[OA\Post(description: 'Creates a new tag and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created tag', content: new OA\JsonContent(ref: '#/components/schemas/TagEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TagEditForm'))]

View File

@@ -47,10 +47,10 @@ final class TeamController extends BaseApiController
}
/**
* Fetch all existing teams (which are visible to the user)
* Fetch collection of teams
*/
#[IsGranted('view_team')]
#[OA\Response(response: 200, description: 'Returns the collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))]
#[OA\Response(response: 200, description: 'Returns a collection of teams', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamCollection')))]
#[Route(methods: ['GET'], path: '', name: 'get_teams')]
public function cgetAction(): Response
{
@@ -66,7 +66,7 @@ final class TeamController extends BaseApiController
}
/**
* Returns one team
* Fetch team
*/
#[IsGranted('view_team')]
#[OA\Response(response: 200, description: 'Returns one team entity', content: new OA\JsonContent(ref: '#/components/schemas/Team'))]
@@ -80,7 +80,7 @@ final class TeamController extends BaseApiController
}
/**
* Delete a team
* Delete team
*/
#[IsGranted('delete_team')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one team')])]
@@ -96,7 +96,7 @@ final class TeamController extends BaseApiController
}
/**
* Creates a new team
* Create team
*/
#[IsGranted('create_team')]
#[OA\Post(description: 'Creates a new team and returns it afterwards', responses: [new OA\Response(response: 200, description: 'Returns the new created team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -125,7 +125,7 @@ final class TeamController extends BaseApiController
}
/**
* Update an existing team
* Update team
*/
#[IsGranted('edit_team')]
#[OA\Patch(description: 'Update an existing team, you can pass all or just a subset of all attributes (passing members will replace all existing ones)', responses: [new OA\Response(response: 200, description: 'Returns the updated team', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -163,7 +163,7 @@ final class TeamController extends BaseApiController
}
/**
* Add a new member to a team
* Add team member
*/
#[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new user to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -187,7 +187,7 @@ final class TeamController extends BaseApiController
}
/**
* Removes a member from the team
* Remove team member
*/
#[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a user from the team. The teamlead cannot be removed.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -215,7 +215,7 @@ final class TeamController extends BaseApiController
}
/**
* Grant the team access to a customer
* Grant team access to customer
*/
#[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new customer to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -238,7 +238,7 @@ final class TeamController extends BaseApiController
}
/**
* Revokes access for a customer from a team
* Revoke customer access from team
*/
#[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a customer from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -261,7 +261,7 @@ final class TeamController extends BaseApiController
}
/**
* Grant the team access to a project
* Grant team access to project
*/
#[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new project to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -284,7 +284,7 @@ final class TeamController extends BaseApiController
}
/**
* Revokes access for a project from a team
* Revoke project access from team
*/
#[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a project from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -307,7 +307,7 @@ final class TeamController extends BaseApiController
}
/**
* Grant the team access to an activity
* Grant team access to activity
*/
#[IsGranted('edit_team')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Adds a new activity to a team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]
@@ -330,7 +330,7 @@ final class TeamController extends BaseApiController
}
/**
* Revokes access for an activity from a team
* Revoke activity access from team
*/
#[IsGranted('edit_team')]
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Removes a activity from the team.', content: new OA\JsonContent(ref: '#/components/schemas/Team'))])]

View File

@@ -68,10 +68,10 @@ final class TimesheetController extends BaseApiController
}
/**
* Returns a collection of timesheet records (which are visible to the user)
* Fetch collection of timesheets
*/
#[IsGranted(new Expression("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')"))]
#[OA\Response(response: 200, description: 'Returns a collection of timesheet records. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))]
#[OA\Response(response: 200, description: 'Returns a collection of timesheets. The datetime fields are given in the users local time including the timezone offset (ISO-8601).', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollection')))]
#[Route(methods: ['GET'], path: '', name: 'get_timesheets')]
#[Rest\QueryParam(name: 'user', requirements: '\d+|all', strict: true, nullable: true, description: "User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")]
#[Rest\QueryParam(name: 'users', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of user IDs to filter, e.g.: users[]=1&users[]=2 (ignored if user=all)')]
@@ -270,11 +270,11 @@ final class TimesheetController extends BaseApiController
}
/**
* Returns one timesheet record
* Return timesheet
*/
#[IsGranted('view', 'timesheet')]
#[OA\Response(response: 200, description: 'Returns one timesheet record. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to fetch', required: true)]
#[OA\Response(response: 200, description: 'Returns one timesheet. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to fetch', required: true)]
#[Route(methods: ['GET'], path: '/{id}', name: 'get_timesheet', requirements: ['id' => '\d+'])]
public function getAction(Timesheet $timesheet): Response
{
@@ -285,10 +285,10 @@ final class TimesheetController extends BaseApiController
}
/**
* Creates a new timesheet record
* Create timesheet
*/
#[IsGranted('create_own_timesheet')]
#[OA\Post(description: 'Creates a new timesheet record for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
#[OA\Post(description: 'Creates a new timesheet for the current user and returns it afterwards.', responses: [new OA\Response(response: 200, description: 'Returns the new created timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
#[Route(methods: ['POST'], path: '', name: 'post_timesheet')]
#[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources (TimesheetExpanded). Allowed values: true (default: false)')]
@@ -338,11 +338,11 @@ final class TimesheetController extends BaseApiController
}
/**
* Update an existing timesheet record
* Update timesheet
*/
#[IsGranted('edit', 'timesheet')]
#[OA\Patch(description: 'Update an existing timesheet record, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to update', required: true)]
#[OA\Patch(description: 'Update timesheet, you can pass all or just a subset of the attributes.', responses: [new OA\Response(response: 200, description: 'Returns the updated timesheet', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to update', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEditForm'))]
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Timesheet $timesheet): Response
@@ -381,11 +381,11 @@ final class TimesheetController extends BaseApiController
}
/**
* Delete an existing timesheet record
* Delete timesheet
*/
#[IsGranted('delete', 'timesheet')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet record')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to delete', required: true)]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one timesheet')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to delete', required: true)]
#[Route(methods: ['DELETE'], path: '/{id}', name: 'delete_timesheet', requirements: ['id' => '\d+'])]
public function deleteAction(Timesheet $timesheet): Response
{
@@ -397,10 +397,10 @@ final class TimesheetController extends BaseApiController
}
/**
* Returns the collection of recent user activities
* Fetch recent user activities
*/
#[IsGranted('view_own_timesheet')]
#[OA\Response(response: 200, description: 'Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
#[OA\Response(response: 200, description: 'Returns a collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
#[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')]
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')]
@@ -431,10 +431,10 @@ final class TimesheetController extends BaseApiController
}
/**
* Returns the collection of active timesheet records
* Fetch active timesheets
*/
#[IsGranted('view_own_timesheet')]
#[OA\Response(response: 200, description: 'Returns the collection of active timesheet records for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
#[OA\Response(response: 200, description: 'Returns a collection of active timesheets for the current user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
#[Route(methods: ['GET'], path: '/active', name: 'active_timesheet')]
public function activeAction(): Response
{
@@ -450,16 +450,17 @@ final class TimesheetController extends BaseApiController
}
/**
* Stops an active timesheet record.
* Stop active timesheet
*
* This route is available via GET and PATCH, as users over and over again run into errors when stopping.
* Likely caused by a slow JS engine and a fast-click after page reload.
*/
#[IsGranted('stop', 'timesheet')]
#[OA\Response(response: 200, description: 'Stops an active timesheet record and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to stop', required: true)]
#[OA\Response(response: 200, description: 'Stops an active timesheet and returns it afterwards.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to stop', required: true)]
#[Route(methods: ['GET'], path: '/{id}/stop', name: 'stop_timesheet_get', requirements: ['id' => '\d+'])]
#[Route(methods: ['PATCH'], path: '/{id}/stop', name: 'stop_timesheet', requirements: ['id' => '\d+'])]
#[OA\Get(x: ['internal' => true])]
public function stopAction(Timesheet $timesheet): Response
{
$this->service->stopTimesheet($timesheet);
@@ -471,11 +472,12 @@ final class TimesheetController extends BaseApiController
}
/**
* Restarts a previously stopped timesheet record for the current user
* Restart a timesheet for the current user
*/
#[IsGranted('start', 'timesheet')]
#[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)]
#[OA\Response(response: 200, description: 'Restart a timesheet for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to restart', required: true)]
#[OA\Get(x: ['internal' => true])]
#[Route(methods: ['GET'], path: '/{id}/restart', name: 'restart_timesheet_get', requirements: ['id' => '\d+'])]
#[Route(methods: ['PATCH'], path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])]
#[Rest\RequestParam(name: 'copy', requirements: 'all', strict: true, nullable: true, description: 'Whether data should be copied to the new entry. Allowed values: all (default: nothing is copied)')]
@@ -532,11 +534,11 @@ final class TimesheetController extends BaseApiController
}
/**
* Duplicates an existing timesheet record
* Duplicate a timesheet
*/
#[IsGranted('duplicate', 'timesheet')]
#[OA\Response(response: 200, description: 'Duplicates a timesheet record, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to duplicate', required: true)]
#[OA\Response(response: 200, description: 'Duplicates a timesheet, resetting the export state only.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to duplicate', required: true)]
#[Route(methods: ['PATCH'], path: '/{id}/duplicate', name: 'duplicate_timesheet', requirements: ['id' => '\d+'])]
public function duplicateAction(Timesheet $timesheet): Response
{
@@ -553,11 +555,11 @@ final class TimesheetController extends BaseApiController
}
/**
* Switch the export state of a timesheet record to (un-)lock it
* Toggle timesheet export state
*/
#[IsGranted('edit_export', 'timesheet')]
#[OA\Response(response: 200, description: 'Switches the exported state on the record and therefor locks / unlocks it for further updates. Needs edit_export_*_timesheet permission.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to switch export state', required: true)]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to switch export state', required: true)]
#[Route(methods: ['PATCH'], path: '/{id}/export', name: 'export_timesheet', requirements: ['id' => '\d+'])]
public function exportAction(Timesheet $timesheet): Response
{
@@ -576,11 +578,11 @@ final class TimesheetController extends BaseApiController
}
/**
* Sets the value of a meta-field for an existing timesheet.
* Update timesheet custom-field
*/
#[IsGranted('edit', 'timesheet')]
#[OA\Response(response: 200, description: 'Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to set the meta-field value for', required: true)]
#[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet ID to set the meta-field value for', required: true)]
#[Route(methods: ['PATCH'], path: '/{id}/meta', requirements: ['id' => '\d+'])]
#[Rest\RequestParam(name: 'name', strict: true, nullable: false, description: 'The meta-field name')]
#[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')]

View File

@@ -47,10 +47,10 @@ final class UserController extends BaseApiController
}
/**
* Returns the collection of users (which are visible to the user)
* Fetch collection of users
*/
#[IsGranted('view_user')]
#[OA\Response(response: 200, description: 'Returns the collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))]
#[OA\Response(response: 200, description: 'Returns a collection of users. Required permission: view_user', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserCollection')))]
#[Route(methods: ['GET'], path: '', name: 'get_users')]
#[Rest\QueryParam(name: 'visible', requirements: '1|2|3', default: 1, strict: true, nullable: true, description: 'Visibility status to filter users: 1=visible, 2=hidden, 3=all')]
#[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)')]
@@ -129,7 +129,7 @@ final class UserController extends BaseApiController
}
/**
* Creates a new user
* Create user
*/
#[IsGranted('create_user')]
#[OA\Post(description: 'Creates a new user and returns it afterwards')]
@@ -199,7 +199,7 @@ final class UserController extends BaseApiController
}
/**
* Delete an API token for the current user
* Delete API token for the current user
*/
#[OA\Delete(responses: [new OA\Response(response: 200, description: 'Success if the token could be deleted.')])]
#[OA\Parameter(name: 'id', in: 'path', description: 'The API token ID to remove', required: true)]

View File

@@ -55,6 +55,18 @@ class ActivityService
return $activity;
}
public function saveActivity(Activity $activity): Activity
{
if ($activity->isNew()) {
return $this->saveNewActivity($activity); // @phpstan-ignore method.deprecated
} else {
return $this->updateActivity($activity); // @phpstan-ignore method.deprecated
}
}
/**
* @deprecated since 2.35 - use saveActivity() instead
*/
public function saveNewActivity(Activity $activity): Activity
{
if (null !== $activity->getId()) {
@@ -89,6 +101,9 @@ class ActivityService
}
}
/**
* @deprecated since 2.35 - use saveActivity() instead
*/
public function updateActivity(Activity $activity): Activity
{
$this->validateActivity($activity);

View File

@@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\ArrayInput;
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\Filesystem\Exception\FileNotFoundException;
@@ -25,8 +26,6 @@ abstract class AbstractBundleInstallerCommand extends Command
{
/**
* Returns the base directory to the Kimai installation.
*
* @return string
*/
protected function getRootDirectory(): string
{
@@ -39,18 +38,26 @@ abstract class AbstractBundleInstallerCommand extends Command
/**
* If your bundle ships assets, that need to be available in the public/ directory,
* then overwrite this method and return: <true>.
*
* @return bool
*/
protected function hasAssets(): bool
{
return false;
}
private function hasMigrations(SymfonyStyle $io): bool
{
$config = $this->getMigrationConfigFilename();
if ($config === null) {
return false;
}
return true;
}
/**
* Returns an absolute filename to your doctrine migrations configuration, if you want to install database tables.
*
* @return string|null
* Returns an absolute filename to your doctrine migrations configuration
* if you want to run database migrations.
*/
protected function getMigrationConfigFilename(): ?string
{
@@ -59,16 +66,12 @@ abstract class AbstractBundleInstallerCommand extends Command
/**
* Returns the bundle short name for the installer command.
*
* @return string
*/
abstract protected function getBundleCommandNamePart(): string;
/**
* Returns the full name fo this command.
* Please stick to the standard and overwrite getBundleCommandNamePart() only.
*
* @return string
*/
protected function getInstallerCommandName(): string
{
@@ -77,8 +80,6 @@ abstract class AbstractBundleInstallerCommand extends Command
/**
* Returns the bundles real name (same as your namespace).
*
* @return string
*/
protected function getBundleName(): string
{
@@ -100,6 +101,8 @@ abstract class AbstractBundleInstallerCommand extends Command
->setName($this->getInstallerCommandName())
->setDescription('Install the bundle: ' . $this->getBundleName())
->setHelp('This command will perform the basic installation steps to get the bundle up and running.')
->addOption('database', null, InputOption::VALUE_NONE, 'Only run the database installation')
->addOption('assets', null, InputOption::VALUE_NONE, 'Only run the asset installation')
;
}
@@ -107,6 +110,15 @@ abstract class AbstractBundleInstallerCommand extends Command
{
$io = new SymfonyStyle($input, $output);
$noAssets = $input->getOption('database');
$onlyAssets = $input->getOption('assets');
if ($noAssets && $onlyAssets) {
$io->error('Options --assets and --database are mutually exclusive');
return Command::FAILURE;
}
// many users execute the bin/console command from arbitrary locations
// this will make sure that relative paths (like doctrine migrations) work as expected
$path = getcwd();
@@ -117,17 +129,19 @@ abstract class AbstractBundleInstallerCommand extends Command
\sprintf('Starting installation of plugin: %s ...', $bundleName)
);
try {
$this->importMigrations($io, $output);
} catch (\Exception $ex) {
$io->error(
\sprintf('Failed to install database for bundle %s. %s', $bundleName, $ex->getMessage())
);
if (!$onlyAssets && $this->hasMigrations($io)) {
try {
$this->importMigrations($io, $output);
} catch (\Exception $ex) {
$io->error(
\sprintf('Failed to install database for bundle %s. %s', $bundleName, $ex->getMessage())
);
return Command::FAILURE;
return Command::FAILURE;
}
}
if ($this->hasAssets()) {
if (!$noAssets && $this->hasAssets()) {
try {
$this->installAssets($io, $output);
} catch (\Exception $ex) {
@@ -148,7 +162,7 @@ abstract class AbstractBundleInstallerCommand extends Command
return Command::SUCCESS;
}
protected function installAssets(SymfonyStyle $io, OutputInterface $output): void
private function installAssets(SymfonyStyle $io, OutputInterface $output): void
{
$command = $this->getApplication()->find('assets:install');
$cmdInput = new ArrayInput([]);
@@ -160,11 +174,11 @@ abstract class AbstractBundleInstallerCommand extends Command
$io->writeln('');
}
protected function importMigrations(SymfonyStyle $io, OutputInterface $output): void
private function importMigrations(SymfonyStyle $io, OutputInterface $output): void
{
$config = $this->getMigrationConfigFilename();
if (null === $config) {
if ($config === null) {
return;
}

View File

@@ -25,7 +25,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*
* @codeCoverageIgnore
*/
#[AsCommand(name: 'kimai:install', description: 'Kimai installation command', aliases: ['kimai:update'])]
#[AsCommand(name: 'kimai:install', description: 'Install and update Kimai', aliases: ['kimai:update'])]
final class InstallCommand extends Command
{
public function __construct(private readonly Connection $connection)

View File

@@ -22,10 +22,13 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*
* @codeCoverageIgnore
*/
#[AsCommand(name: 'kimai:reload')]
#[AsCommand(name: 'kimai:reload', description: 'Reload Kimai caches')]
final class ReloadCommand extends Command
{
public function __construct(private string $projectDirectory, private string $kernelEnvironment)
public function __construct(
private readonly string $projectDirectory,
private readonly string $kernelEnvironment
)
{
parent::__construct();
}
@@ -42,10 +45,7 @@ final class ReloadCommand extends Command
protected function configure(): void
{
$this
->setDescription('Reload Kimai caches')
->setHelp('This command will validate the configurations and translations and then clear and rebuild the application cache.')
;
$this->setHelp('This will validate configurations and translations and then clear and rebuild the application cache.');
}
protected function execute(InputInterface $input, OutputInterface $output): int

View File

@@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.34.0';
public const VERSION = '2.35.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 23400;
public const VERSION_ID = 23500;
/**
* The software name
*/

View File

@@ -271,7 +271,7 @@ final class ActivityController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
try {
$this->activityService->saveNewActivity($activity);
$this->activityService->saveActivity($activity);
$this->flashSuccess('action.update.success');
return $this->redirectToRouteAfterCreate('activity_details', ['id' => $activity->getId()]);
@@ -300,7 +300,7 @@ final class ActivityController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->activityService->updateActivity($activity);
$this->activityService->saveActivity($activity);
$this->flashSuccess('action.update.success');
if ($this->isGranted('view', $activity)) {
@@ -355,7 +355,7 @@ final class ActivityController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
try {
$this->activityService->updateActivity($activity);
$this->activityService->saveActivity($activity);
$this->flashSuccess('action.update.success');
if ($this->isGranted('view', $activity)) {

View File

@@ -154,7 +154,7 @@ final class ProjectController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->projectService->updateProject($project);
$this->projectService->saveProject($project);
$this->flashSuccess('action.update.success');
if ($this->isGranted('view', $project)) {
@@ -197,7 +197,7 @@ final class ProjectController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
try {
$this->projectService->saveNewProject($project, new Context($this->getUser()));
$this->projectService->saveProject($project, new Context($this->getUser()));
$this->flashSuccess('action.update.success');
return $this->redirectToRouteAfterCreate('project_details', ['id' => $project->getId()]);
@@ -454,7 +454,7 @@ final class ProjectController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
try {
$this->projectService->updateProject($project);
$this->projectService->saveProject($project);
$this->flashSuccess('action.update.success');
if ($this->isGranted('view', $project)) {

View File

@@ -59,6 +59,18 @@ final class CustomerService
return $customer;
}
public function saveCustomer(Customer $customer): Customer
{
if ($customer->isNew()) {
return $this->saveNewCustomer($customer); // @phpstan-ignore method.deprecated
} else {
return $this->updateCustomer($customer); // @phpstan-ignore method.deprecated
}
}
/**
* @deprecated since 2.35 - use saveCustomer() instead
*/
public function saveNewCustomer(Customer $customer): Customer
{
if (null !== $customer->getId()) {
@@ -93,6 +105,9 @@ final class CustomerService
}
}
/**
* @deprecated since 2.35 - use saveCustomer() instead
*/
public function updateCustomer(Customer $customer): Customer
{
$this->validateCustomer($customer);

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\AccessTokenRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
@@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(fields: ['token'])]
class AccessToken
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
@@ -29,17 +30,17 @@ class AccessToken
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private User $user;
#[ORM\Column(name: 'token', type: 'string', length: 100, nullable: false)]
#[ORM\Column(name: 'token', type: Types::STRING, length: 100, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 100)]
private string $token;
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 50)]
private ?string $name = null;
#[ORM\Column(name: 'last_usage', type: 'datetime_immutable', nullable: true)]
#[ORM\Column(name: 'last_usage', type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $lastUsage = null;
#[ORM\Column(name: 'expires_at', type: 'datetime_immutable', nullable: true)]
#[ORM\Column(name: 'expires_at', type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $expiresAt = null;
public function __construct(User $user, string $token)

View File

@@ -16,6 +16,7 @@ use App\Repository\ActivityRepository;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -42,7 +43,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Unique activity ID
*/
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
@@ -58,7 +59,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Name of this activity
*/
#[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 150, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 150)]
#[Serializer\Expose]
@@ -68,7 +69,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Description of this activity
*/
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
#[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'comment')]
@@ -76,13 +77,13 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Whether this activity is visible and can be selected
*/
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'visible', type: 'boolean')]
private bool $visible = true;
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -113,9 +114,9 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Activity'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
private Collection $teams;
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
#[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)]
private ?string $invoiceText = null;
#[ORM\Column(name: 'number', type: 'string', length: 10, nullable: true)]
#[ORM\Column(name: 'number', type: Types::STRING, length: 10, nullable: true)]
#[Assert\Length(max: 10)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -134,6 +135,11 @@ class Activity implements EntityWithMetaFields, EntityWithBudget, CreatedAt
return $this->id;
}
public function isNew(): bool
{
return $this->id === null;
}
public function getProject(): ?Project
{
return $this->project;

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\BookmarkRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
@@ -25,7 +26,7 @@ class Bookmark
public const COLUMN_VISIBILITY = 'columns';
public const TIMESHEET = 'timesheet';
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
@@ -33,15 +34,15 @@ class Bookmark
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $user = null;
#[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
#[ORM\Column(name: 'type', type: Types::STRING, length: 20, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 20)]
private ?string $type = null;
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 50)]
private ?string $name = null;
#[ORM\Column(name: 'content', type: 'text', nullable: false)]
#[ORM\Column(name: 'content', type: Types::TEXT, nullable: false)]
private ?string $content = null;
public function getId(): ?int

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Export\Annotation as Exporter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
@@ -19,7 +20,7 @@ trait BudgetTrait
/**
* The total monetary budget, will be zero if not configured.
*/
#[ORM\Column(name: 'budget', type: 'float', nullable: false)]
#[ORM\Column(name: 'budget', type: Types::FLOAT, nullable: false)]
#[Assert\Range(min: 0.00, max: 900000000000.00)]
#[Assert\NotNull]
#[Serializer\Expose]
@@ -29,7 +30,7 @@ trait BudgetTrait
/**
* The time budget in seconds, will be zero if not configured.
*/
#[ORM\Column(name: 'time_budget', type: 'integer', nullable: false)]
#[ORM\Column(name: 'time_budget', type: Types::INTEGER, nullable: false)]
#[Assert\Range(min: 0, max: 2145600000)]
#[Assert\NotNull]
#[Serializer\Expose]
@@ -41,7 +42,7 @@ trait BudgetTrait
* - null = default / full time
* - month = monthly budget
*/
#[ORM\Column(name: 'budget_type', type: 'string', length: 10, nullable: true)]
#[ORM\Column(name: 'budget_type', type: Types::STRING, length: 10, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])]
#[Exporter\Expose(label: 'budgetType')]

View File

@@ -12,6 +12,7 @@ namespace App\Entity;
use App\Constants;
use App\Export\Annotation as Exporter;
use App\Validator\Constraints as Constraints;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
@@ -20,7 +21,7 @@ trait ColorTrait
/**
* The assigned color in HTML hex format, e.g. #dd1d00
*/
#[ORM\Column(name: 'color', type: 'string', length: 7, nullable: true)]
#[ORM\Column(name: 'color', type: Types::STRING, length: 7, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'color')]

View File

@@ -9,6 +9,7 @@
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
@@ -16,19 +17,19 @@ trait CommentTableTypeTrait
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(name: 'message', type: 'text', nullable: false)]
#[ORM\Column(name: 'message', type: Types::TEXT, nullable: false)]
#[Assert\NotNull]
private ?string $message = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $createdBy = null;
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
#[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE, nullable: false)]
#[Assert\NotNull]
private ?\DateTime $createdAt = null;
#[ORM\Column(name: 'pinned', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'pinned', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
#[Assert\NotNull]
private bool $pinned = false;

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\ConfigurationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
@@ -23,13 +24,13 @@ class Configuration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 100, nullable: false)]
#[Assert\NotNull]
#[Assert\Length(min: 2, max: 100)]
private ?string $name = null;
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
#[ORM\Column(name: 'value', type: Types::TEXT, length: 65535, nullable: true)]
#[Assert\Length(max: 65535)]
private ?string $value = null;

View File

@@ -16,6 +16,7 @@ use App\Repository\CustomerRepository;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -36,50 +37,50 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
use ColorTrait;
use CreatedTrait;
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')]
private ?int $id = null;
#[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 150, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 150)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'name')]
private ?string $name = null;
#[ORM\Column(name: 'number', type: 'string', length: 50, nullable: true)]
#[ORM\Column(name: 'number', type: Types::STRING, length: 50, nullable: true)]
#[Assert\Length(max: 50)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'number')]
private ?string $number = null;
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
#[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'comment')]
private ?string $comment = null;
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false)]
#[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'visible', type: 'boolean')]
private bool $visible = true;
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'billable', type: 'boolean')]
private bool $billable = true;
#[ORM\Column(name: 'company', type: 'string', length: 100, nullable: true)]
#[ORM\Column(name: 'company', type: Types::STRING, length: 100, nullable: true)]
#[Assert\Length(max: 100)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'company')]
private ?string $company = null;
#[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)]
#[ORM\Column(name: 'vat_id', type: Types::STRING, length: 50, nullable: true)]
#[Assert\Length(max: 50)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
@@ -96,7 +97,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'address')]
private ?string $address = null;
#[ORM\Column(name: 'country', type: 'string', length: 2, nullable: false)]
#[ORM\Column(name: 'country', type: Types::STRING, length: 2, nullable: false)]
#[Assert\NotBlank]
#[Assert\Country]
#[Assert\Length(max: 2)]
@@ -104,7 +105,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'country')]
private ?string $country = null;
#[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)]
#[ORM\Column(name: 'currency', type: Types::STRING, length: 3, nullable: false)]
#[Assert\NotBlank]
#[Assert\Currency]
#[Assert\Length(max: 3)]
@@ -112,19 +113,19 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Customer'])]
#[Exporter\Expose(label: 'currency')]
private ?string $currency = self::DEFAULT_CURRENCY;
#[ORM\Column(name: 'phone', type: 'string', length: 30, nullable: true)]
#[ORM\Column(name: 'phone', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'phone')]
private ?string $phone = null;
#[ORM\Column(name: 'fax', type: 'string', length: 30, nullable: true)]
#[ORM\Column(name: 'fax', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'fax')]
private ?string $fax = null;
#[ORM\Column(name: 'mobile', type: 'string', length: 30, nullable: true)]
#[ORM\Column(name: 'mobile', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
@@ -133,13 +134,13 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Customers contact email
*/
#[ORM\Column(name: 'email', type: 'string', length: 75, nullable: true)]
#[ORM\Column(name: 'email', type: Types::STRING, length: 75, nullable: true)]
#[Assert\Length(max: 75)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'email')]
private ?string $email = null;
#[ORM\Column(name: 'homepage', type: 'string', length: 100, nullable: true)]
#[ORM\Column(name: 'homepage', type: Types::STRING, length: 100, nullable: true)]
#[Assert\Length(max: 100)]
#[Serializer\Expose]
#[Serializer\Groups(['Customer_Entity'])]
@@ -148,7 +149,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Timezone of begin and end
*/
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
#[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)]
#[Assert\NotBlank]
#[Assert\Timezone]
#[Assert\Length(max: 64)]
@@ -187,7 +188,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?InvoiceTemplate $invoiceTemplate = null;
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
#[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)]
private ?string $invoiceText = null;
public function __construct(string $name)
@@ -203,6 +204,11 @@ class Customer implements EntityWithMetaFields, EntityWithBudget, CreatedAt
return $this->id;
}
public function isNew(): bool
{
return $this->id === null;
}
public function setName(?string $name): void
{
$this->name = $name;

View File

@@ -14,6 +14,7 @@ use App\Invoice\InvoiceModel;
use App\Repository\InvoiceRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -44,20 +45,20 @@ class Invoice implements EntityWithMetaFields
/**
* Unique invoice ID
*/
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')]
private ?int $id = null;
#[ORM\Column(name: 'invoice_number', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'invoice_number', type: Types::STRING, length: 50, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'invoice.number', type: 'string')]
private ?string $invoiceNumber = null;
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
#[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Invoice'])]
#[Exporter\Expose(label: 'comment')]
@@ -76,53 +77,53 @@ class Invoice implements EntityWithMetaFields
#[Serializer\Groups(['Default'])]
#[OA\Property(ref: '#/components/schemas/User')]
private ?User $user = null;
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
#[ORM\Column(name: 'created_at', type: Types::DATETIME_MUTABLE, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?\DateTime $createdAt = null;
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
#[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)]
private ?string $timezone = null;
#[ORM\Column(name: 'total', type: 'float', nullable: false)]
#[ORM\Column(name: 'total', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'total_rate', type: 'float')]
private float $total = 0.00;
#[ORM\Column(name: 'tax', type: 'float', nullable: false)]
#[ORM\Column(name: 'tax', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'invoice.tax', type: 'float')]
private float $tax = 0.00;
#[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)]
#[ORM\Column(name: 'currency', type: Types::STRING, length: 3, nullable: false)]
#[Assert\NotNull]
#[Assert\Length(max: 3)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'currency', type: 'string')]
private ?string $currency = null;
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
#[ORM\Column(name: 'due_days', type: Types::INTEGER, length: 3, nullable: false)]
#[Assert\NotNull]
#[Assert\Range(min: 0, max: 999)]
#[Serializer\Expose]
#[Serializer\Groups(['Invoice'])]
#[Exporter\Expose(label: 'due_days', type: 'integer')]
private int $dueDays = 30;
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
#[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull]
#[Assert\Range(min: 0.0, max: 99.99)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'tax_rate', type: 'float')]
private float $vat = 0.00;
#[ORM\Column(name: 'status', type: 'string', length: 20, nullable: false)]
#[ORM\Column(name: 'status', type: Types::STRING, length: 20, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'status', type: 'string')]
private string $status = self::STATUS_NEW;
#[ORM\Column(name: 'invoice_filename', type: 'string', length: 150, nullable: false)]
#[ORM\Column(name: 'invoice_filename', type: Types::STRING, length: 150, nullable: false)]
#[Assert\NotNull]
#[Assert\Length(min: 1, max: 150)]
#[Exporter\Expose(label: 'file', type: 'string')]

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\InvoiceTemplateRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
@@ -21,15 +22,15 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity('name')]
class InvoiceTemplate
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
#[ORM\Column(name: 'name', type: 'string', length: 60, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 60, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 1, max: 60)]
private ?string $name = null;
#[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)]
#[ORM\Column(name: 'title', type: Types::STRING, length: 255, nullable: false)]
#[Assert\Length(max: 255)]
#[Assert\NotBlank]
private ?string $title = null;
@@ -37,41 +38,41 @@ class InvoiceTemplate
#[Assert\Length(max: 255)]
#[Assert\NotBlank]
private ?string $company = null;
#[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)]
#[ORM\Column(name: 'vat_id', type: Types::STRING, length: 50, nullable: true)]
#[Assert\Length(max: 50)]
private ?string $vatId = null;
#[ORM\Column(name: 'address', type: 'text', nullable: true)]
#[ORM\Column(name: 'address', type: Types::TEXT, nullable: true)]
private ?string $address = null;
#[ORM\Column(name: 'contact', type: 'text', nullable: true)]
#[ORM\Column(name: 'contact', type: Types::TEXT, nullable: true)]
private ?string $contact = null;
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
#[ORM\Column(name: 'due_days', type: Types::INTEGER, length: 3, nullable: false)]
#[Assert\NotNull]
#[Assert\Range(min: 0, max: 999)]
private ?int $dueDays = 30;
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
#[ORM\Column(name: 'vat', type: Types::FLOAT, nullable: false)]
#[Assert\NotNull]
#[Assert\Range(min: 0.0, max: 99.99)]
private ?float $vat = 0.00;
#[ORM\Column(name: 'calculator', type: 'string', length: 20, nullable: false)]
#[ORM\Column(name: 'calculator', type: Types::STRING, length: 20, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(max: 20)]
private string $calculator = 'default';
#[ORM\Column(name: 'number_generator', type: 'string', length: 20, nullable: false)]
#[ORM\Column(name: 'number_generator', type: Types::STRING, length: 20, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(max: 20)]
private string $numberGenerator = 'default';
#[ORM\Column(name: 'renderer', type: 'string', length: 20, nullable: false)]
#[ORM\Column(name: 'renderer', type: Types::STRING, length: 20, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(max: 20)]
private string $renderer = 'default';
#[ORM\Column(name: 'payment_terms', type: 'text', nullable: true)]
#[ORM\Column(name: 'payment_terms', type: Types::TEXT, nullable: true)]
private ?string $paymentTerms = null;
#[ORM\Column(name: 'payment_details', type: 'text', nullable: true)]
#[ORM\Column(name: 'payment_details', type: Types::TEXT, nullable: true)]
private ?string $paymentDetails = null;
/**
* Used for translations and formatting money, numbers, dates and time.
*/
#[ORM\Column(name: 'language', type: 'string', length: 6, nullable: false)]
#[ORM\Column(name: 'language', type: Types::STRING, length: 6, nullable: false)]
#[Assert\NotBlank]
private ?string $language = 'en';

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Form\Type\YesNoType;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -22,13 +23,13 @@ trait MetaTableTypeTrait
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[Serializer\Exclude]
private ?int $id = null;
/**
* Name of the meta (custom) field
*/
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)]
#[Assert\NotNull]
#[Assert\Length(min: 2, max: 50)]
#[Serializer\Expose]
@@ -37,12 +38,12 @@ trait MetaTableTypeTrait
/**
* Value of the meta (custom) field
*/
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
#[ORM\Column(name: 'value', type: Types::TEXT, length: 65535, nullable: true)]
#[Assert\Length(max: 65535)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?string $value = null;
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
#[Assert\NotNull]
private bool $visible = false;
private ?string $label = null;

View File

@@ -16,6 +16,7 @@ use App\Repository\ProjectRepository;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -41,7 +42,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Unique Project ID
*/
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
@@ -61,7 +62,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Project name
*/
#[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 150, nullable: false)]
#[Assert\NotNull]
#[Assert\Length(min: 2, max: 150)]
#[Serializer\Expose]
@@ -71,7 +72,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* Project order number
*/
#[ORM\Column(name: 'order_number', type: 'text', length: 50, nullable: true)]
#[ORM\Column(name: 'order_number', type: Types::TEXT, length: 50, nullable: true)]
#[Assert\Length(max: 50)]
#[Serializer\Expose]
#[Serializer\Groups(['Project_Entity'])]
@@ -82,7 +83,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/
#[ORM\Column(name: 'order_date', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'order_date', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Project_Entity'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
@@ -93,7 +94,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/
#[ORM\Column(name: 'start', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'start', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Project'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
@@ -104,16 +105,16 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/
#[ORM\Column(name: 'end', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'end', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Project'])]
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
#[Serializer\Accessor(getter: 'getEnd')]
private ?\DateTime $end = null;
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: true)]
#[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: true)]
private ?string $timezone = null;
private bool $localized = false;
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
#[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'comment')]
@@ -121,13 +122,13 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
/**
* If the project is not visible, times cannot be recorded
*/
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false)]
#[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'visible', type: 'boolean')]
private bool $visible = true;
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -158,17 +159,17 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
#[Serializer\Groups(['Project'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
private Collection $teams;
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
#[ORM\Column(name: 'invoice_text', type: Types::TEXT, nullable: true)]
private ?string $invoiceText = null;
/**
* Whether this project allows booking of global activities
*/
#[ORM\Column(name: 'global_activities', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'global_activities', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private bool $globalActivities = true;
#[ORM\Column(name: 'number', type: 'string', length: 10, nullable: true)]
#[ORM\Column(name: 'number', type: Types::STRING, length: 10, nullable: true)]
#[Assert\Length(max: 10)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -187,6 +188,11 @@ class Project implements EntityWithMetaFields, EntityWithBudget, CreatedAt
return $this->id;
}
public function isNew(): bool
{
return $this->id === null;
}
public function getCustomer(): ?Customer
{
return $this->customer;

View File

@@ -9,6 +9,7 @@
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -16,7 +17,7 @@ use Symfony\Component\Validator\Constraints as Assert;
trait Rate
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
@@ -28,16 +29,16 @@ trait Rate
#[Serializer\Groups(['Default'])]
#[OA\Property(ref: '#/components/schemas/User')]
private ?User $user = null;
#[ORM\Column(name: 'rate', type: 'float', nullable: false)]
#[ORM\Column(name: 'rate', type: Types::FLOAT, nullable: false)]
#[Assert\GreaterThanOrEqual(0)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private float $rate = 0.00;
#[ORM\Column(name: 'internal_rate', type: 'float', nullable: true)]
#[ORM\Column(name: 'internal_rate', type: Types::FLOAT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?float $internalRate = null;
#[ORM\Column(name: 'fixed', type: 'boolean', nullable: false)]
#[ORM\Column(name: 'fixed', type: Types::BOOLEAN, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\RoleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
@@ -21,11 +22,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity('name')]
class Role
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)]
#[Assert\NotNull]
#[Assert\NotBlank]
#[Assert\Length(min: 5, max: 50)]

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\RolePermissionRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
@@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(['role', 'permission'])]
class RolePermission
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
@@ -29,10 +30,10 @@ class RolePermission
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?Role $role = null;
#[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'permission', type: Types::STRING, length: 50, nullable: false)]
#[Assert\Length(max: 50)]
private ?string $permission = null;
#[ORM\Column(name: 'allowed', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'allowed', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
#[Assert\NotNull]
private bool $allowed = false;

View File

@@ -11,6 +11,7 @@ namespace App\Entity;
use App\Repository\TagRepository;
use App\Utils\Color;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
@@ -28,7 +29,7 @@ class Tag
/**
* Internal Tag ID
*/
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
@@ -37,14 +38,14 @@ class Tag
/**
* The tag name
*/
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 100, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
#[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?string $name = null;
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'visible', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]

View File

@@ -13,6 +13,7 @@ use App\Repository\TeamRepository;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -28,7 +29,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[Constraints\Team]
class Team
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
@@ -37,7 +38,7 @@ class Team
/**
* Team name
*/
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 100, nullable: false)]
#[Assert\NotBlank]
#[Assert\Length(min: 2, max: 100)]
#[Serializer\Expose]

View File

@@ -9,6 +9,7 @@
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -21,7 +22,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[Serializer\ExclusionPolicy('all')]
class TeamMember
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
@@ -39,7 +40,7 @@ class TeamMember
#[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
#[OA\Property(ref: '#/components/schemas/Team')]
private ?Team $team = null;
#[ORM\Column(name: 'teamlead', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'teamlead', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity', 'User_Entity'])]

View File

@@ -17,6 +17,7 @@ use DateTime;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
@@ -81,7 +82,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
/**
* Unique Timesheet ID
*/
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[Serializer\Expose]
@@ -91,7 +92,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
* Reflects the date in the user timezone (not in UTC).
* This value is automatically set through the begin column and ONLY used in statistic queries.
*/
#[ORM\Column(name: 'date_tz', type: 'date_immutable', nullable: false)]
#[ORM\Column(name: 'date_tz', type: Types::DATE_IMMUTABLE, nullable: false)]
#[Assert\NotNull]
private ?\DateTimeImmutable $date = null;
/**
@@ -99,7 +100,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/
#[ORM\Column(name: 'start_time', type: 'datetime', nullable: false)]
#[ORM\Column(name: 'start_time', type: Types::DATETIME_MUTABLE, nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -111,7 +112,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
*
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
*/
#[ORM\Column(name: 'end_time', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'end_time', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Serializer\Type(name: 'DateTime')]
@@ -120,18 +121,18 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
/**
* @internal for storing the timezone of "begin" and "end" date
*/
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
#[ORM\Column(name: 'timezone', type: Types::STRING, length: 64, nullable: false)]
#[Assert\Timezone]
private ?string $timezone = null;
/**
* @internal for storing the localized state of dates (see $timezone)
*/
private bool $localized = false;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
#[ORM\Column(name: 'duration', type: Types::INTEGER, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?int $duration = 0;
#[ORM\Column(name: 'break', type: 'integer', nullable: true)]
#[ORM\Column(name: 'break', type: Types::INTEGER, nullable: true)]
private ?int $break = 0;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
@@ -154,36 +155,36 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
#[Serializer\Groups(['Subresource', 'Expanded'])]
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
private ?Project $project = null;
#[ORM\Column(name: 'description', type: 'text', nullable: true)]
#[ORM\Column(name: 'description', type: Types::TEXT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?string $description = null;
#[ORM\Column(name: 'rate', type: 'float', nullable: false)]
#[ORM\Column(name: 'rate', type: Types::FLOAT, nullable: false)]
#[Assert\GreaterThanOrEqual(0)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private float $rate = 0.00;
#[ORM\Column(name: 'internal_rate', type: 'float', nullable: true)]
#[ORM\Column(name: 'internal_rate', type: Types::FLOAT, nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?float $internalRate = null;
#[ORM\Column(name: 'fixed_rate', type: 'float', nullable: true)]
#[ORM\Column(name: 'fixed_rate', type: Types::FLOAT, nullable: true)]
#[Assert\GreaterThanOrEqual(0)]
#[Serializer\Expose]
#[Serializer\Groups(['Entity'])]
private ?float $fixedRate = null;
#[ORM\Column(name: 'hourly_rate', type: 'float', nullable: true)]
#[ORM\Column(name: 'hourly_rate', type: Types::FLOAT, nullable: true)]
#[Assert\GreaterThanOrEqual(0)]
#[Serializer\Expose]
#[Serializer\Groups(['Entity'])]
private ?float $hourlyRate = null;
#[ORM\Column(name: 'exported', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'exported', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private bool $exported = false;
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
#[ORM\Column(name: 'billable', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -193,7 +194,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
*/
#[Assert\NotNull]
private ?string $billableMode = self::BILLABLE_DEFAULT;
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
#[ORM\Column(name: 'category', type: Types::STRING, length: 10, nullable: false, options: ['default' => 'work'])]
#[Assert\NotNull]
private ?string $category = self::WORK;
/**

View File

@@ -17,6 +17,7 @@ use App\WorkingTime\Mode\WorkingTimeModeNone;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use JMS\Serializer\Annotation as Serializer;
@@ -70,7 +71,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')]
@@ -78,7 +79,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
/**
* The user alias will be displayed in the frontend instead of the username
*/
#[ORM\Column(name: 'alias', type: 'string', length: 60, nullable: true)]
#[ORM\Column(name: 'alias', type: Types::STRING, length: 60, nullable: true)]
#[Assert\Length(max: 60)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -87,13 +88,13 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
/**
* Registration date for the user
*/
#[ORM\Column(name: 'registration_date', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'registration_date', type: Types::DATETIME_MUTABLE, nullable: true)]
#[Exporter\Expose(label: 'profile.registration_date', type: 'datetime')]
private ?\DateTime $registeredAt = null;
/**
* An additional title for the user, like the Job position or Department
*/
#[ORM\Column(name: 'title', type: 'string', length: 50, nullable: true)]
#[ORM\Column(name: 'title', type: Types::STRING, length: 50, nullable: true)]
#[Assert\Length(max: 50)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
@@ -102,7 +103,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
/**
* URL to the user avatar, will be auto-generated if empty
*/
#[ORM\Column(name: 'avatar', type: 'string', length: 255, nullable: true)]
#[ORM\Column(name: 'avatar', type: Types::STRING, length: 255, nullable: true)]
#[Assert\Length(max: 255, groups: ['Profile'])]
#[Serializer\Expose]
#[Serializer\Groups(['User_Entity'])]
@@ -110,7 +111,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
/**
* API token (password) for this user
*/
#[ORM\Column(name: 'api_token', type: 'string', length: 255, nullable: true)]
#[ORM\Column(name: 'api_token', type: Types::STRING, length: 255, nullable: true)]
private ?string $apiToken = null;
/**
* @internal to be set via form, must not be persisted
@@ -148,7 +149,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
*
* @internal for internal usage only
*/
#[ORM\Column(name: 'auth', type: 'string', length: 20, nullable: true)]
#[ORM\Column(name: 'auth', type: Types::STRING, length: 20, nullable: true)]
#[Assert\Length(max: 20)]
private ?string $auth = self::AUTH_INTERNAL;
/**
@@ -157,32 +158,32 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
* @internal has no database mapping as the value is calculated from a permission
*/
private ?bool $isAllowedToSeeAllData = null;
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
#[ORM\Column(name: 'username', type: Types::STRING, length: 180, nullable: false)]
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Length(min: 2, max: 64, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?string $username = null;
#[ORM\Column(name: 'email', type: 'string', length: 180, nullable: false)]
#[ORM\Column(name: 'email', type: Types::STRING, length: 180, nullable: false)]
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Length(min: 2, max: 180)]
#[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])]
private ?string $email = null;
#[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)]
#[ORM\Column(name: 'account', type: Types::STRING, length: 30, nullable: true)]
#[Assert\Length(max: 30)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'account_number')]
private ?string $accountNumber = null;
#[ORM\Column(name: 'enabled', type: 'boolean', nullable: false)]
#[ORM\Column(name: 'enabled', type: Types::BOOLEAN, nullable: false)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private bool $enabled = false;
/**
* Encrypted password. Must be persisted.
*/
#[ORM\Column(name: 'password', type: 'string', nullable: false)]
#[ORM\Column(name: 'password', type: Types::STRING, nullable: false)]
private ?string $password = null;
/**
* Plain password. Used for model validation, not persisted.
@@ -190,20 +191,20 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
#[Assert\NotBlank(groups: ['Registration', 'PasswordUpdate', 'UserCreate'])]
#[Assert\Length(min: 8, max: 60, groups: ['Registration', 'PasswordUpdate', 'UserCreate', 'ResetPassword', 'ChangePassword'])]
private ?string $plainPassword = null;
#[ORM\Column(name: 'last_login', type: 'datetime', nullable: true)]
#[ORM\Column(name: 'last_login', type: Types::DATETIME_MUTABLE, nullable: true)]
private ?DateTime $lastLogin = null;
/**
* Random string sent to the user email address in order to verify it.
*/
#[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)]
#[ORM\Column(name: 'confirmation_token', type: Types::STRING, length: 180, unique: true, nullable: true)]
#[Assert\Length(max: 180)]
private ?string $confirmationToken = null;
#[ORM\Column(name: 'password_requested_at', type: 'datetime_immutable', nullable: true)]
#[ORM\Column(name: 'password_requested_at', type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $passwordRequestedAt = null;
/**
* List of all role names
*/
#[ORM\Column(name: 'roles', type: 'array', nullable: false)]
#[ORM\Column(name: 'roles', type: Types::ARRAY, nullable: false)] // @phpstan-ignore classConstant.deprecated
#[Serializer\Expose]
#[Serializer\Groups(['User_Entity'])]
#[Serializer\Type('array<string>')]
@@ -213,11 +214,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
* If not empty two-factor authentication is enabled.
* TODO reduce the length, which was initially forgotten and set to 255, as this is the default for MySQL with Doctrine (see migration Version20230126002049)
*/
#[ORM\Column(name: 'totp_secret', type: 'string', length: 255, nullable: true)]
#[ORM\Column(name: 'totp_secret', type: Types::STRING, length: 255, nullable: true)]
private ?string $totpSecret = null;
#[ORM\Column(name: 'totp_enabled', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'totp_enabled', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
private bool $totpEnabled = false;
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
#[ORM\Column(name: 'system_account', type: Types::BOOLEAN, nullable: false, options: ['default' => false])]
private bool $systemAccount = false;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]

View File

@@ -11,6 +11,7 @@ namespace App\Entity;
use App\Form\Type\YesNoType;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -53,19 +54,19 @@ class UserPreference
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'preferences')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $user = null;
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
#[ORM\Column(name: 'name', type: Types::STRING, length: 50, nullable: false)]
#[Assert\NotNull]
#[Assert\Length(min: 2, max: 50)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private string $name;
#[ORM\Column(name: 'value', type: 'string', length: 255, nullable: true)]
#[ORM\Column(name: 'value', type: Types::STRING, length: 255, nullable: true)]
#[Assert\Length(max: 250)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Repository\WorkingTimeRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
@@ -23,25 +24,25 @@ class WorkingTime
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $user = null;
#[ORM\Column(name: 'date', type: 'date', nullable: false)]
#[ORM\Column(name: 'date', type: Types::DATE_MUTABLE, nullable: false)]
#[Assert\NotNull]
private \DateTimeInterface $date;
#[ORM\Column(name: 'expected', type: 'integer', nullable: false)]
#[ORM\Column(name: 'expected', type: Types::INTEGER, nullable: false)]
#[Assert\NotNull]
private int $expectedTime = 0;
#[ORM\Column(name: 'actual', type: 'integer', nullable: false)]
#[ORM\Column(name: 'actual', type: Types::INTEGER, nullable: false)]
#[Assert\NotNull]
private int $actualTime = 0;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
private ?User $approvedBy = null;
#[ORM\Column(name: 'approved_at', type: 'datetime_immutable', nullable: true)]
#[ORM\Column(name: 'approved_at', type: Types::DATETIME_IMMUTABLE, nullable: true)]
#[Assert\NotNull]
private ?\DateTimeImmutable $approvedAt = null;

View File

@@ -14,7 +14,6 @@ use App\Entity\Activity;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
* @method Activity getEntity()
*/
class ActivityBudgetStatisticModel extends BudgetStatisticModel

View File

@@ -14,8 +14,6 @@ use App\Model\Statistic\BudgetStatistic;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
*/
class BudgetStatisticModel implements BudgetStatisticModelInterface
{

View File

@@ -9,9 +9,6 @@
namespace App\Model;
/**
* @internal do not use in plugins, no BC promise given!
*/
interface BudgetStatisticModelInterface
{
public function isMonthlyBudget(): bool;

View File

@@ -14,7 +14,6 @@ use App\Entity\Customer;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
* @method Customer getEntity()
*/
class CustomerBudgetStatisticModel extends BudgetStatisticModel

View File

@@ -14,7 +14,6 @@ use App\Entity\Project;
/**
* Object used to unify the access to budget data in charts.
*
* @internal do not use in plugins, no BC promise given!
* @method Project getEntity()
*/
class ProjectBudgetStatisticModel extends BudgetStatisticModel

View File

@@ -46,7 +46,7 @@ final class ProjectDuplicationService
$newProject->setEnd(null);
}
$this->projectService->saveNewProject($newProject);
$this->projectService->saveProject($newProject);
foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) {
$newRate = clone $rate;

View File

@@ -56,6 +56,18 @@ final class ProjectService
return $project;
}
public function saveProject(Project $project, ?Context $context = null): Project
{
if ($project->isNew()) {
return $this->saveNewProject($project, $context); // @phpstan-ignore method.deprecated
} else {
return $this->updateProject($project); // @phpstan-ignore method.deprecated
}
}
/**
* @deprecated since 2.35 - use saveProject() instead
*/
public function saveNewProject(Project $project, ?Context $context = null): Project
{
if (null !== $project->getId()) {
@@ -97,6 +109,9 @@ final class ProjectService
}
}
/**
* @deprecated since 2.35 - use saveProject() instead
*/
public function updateProject(Project $project): Project
{
$this->validateProject($project);
@@ -108,8 +123,12 @@ final class ProjectService
return $project;
}
public function findProjectByName(string $name): ?Project
public function findProjectByName(string $name, ?Customer $customer): ?Project
{
if ($customer !== null) {
return $this->repository->findOneBy(['name' => $name, 'customer' => $customer->getId()]);
}
return $this->repository->findOneBy(['name' => $name]);
}

View File

@@ -152,8 +152,6 @@ class ActivityRepository extends EntityRepository
/**
* Returns a query builder that is used for ActivityType and your own 'query_builder' option.
*
* @internal
*/
public function getQueryBuilderForFormType(ActivityFormTypeQuery $query): QueryBuilder
{

View File

@@ -134,8 +134,6 @@ class CustomerRepository extends EntityRepository
/**
* Returns a query builder that is used for CustomerType and your own 'query_builder' option.
*
* @internal
*/
public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
{

View File

@@ -147,8 +147,6 @@ class ProjectRepository extends EntityRepository
/**
* Returns a query builder that is used for ProjectType and your own 'query_builder' option.
*
* @internal
*/
public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder
{

View File

@@ -18,9 +18,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
/**
* @internal
*/
final class TimesheetResult
{
private ?TimesheetResultStatistic $statisticCache = null;