enhanced plugin support (#634)
- refactored admin controller and templates - plugin support for entity actions - changed column mail to email in customer table - refactored theme events
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
/bin/*
|
||||
!bin/console
|
||||
/config/packages/local.yaml
|
||||
/config/packages/*/local.yaml
|
||||
|
||||
templates/invoice/renderer/.~lock*
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
"friendsofsymfony/rest-bundle": "^2.3",
|
||||
"friendsofsymfony/user-bundle": "~2.0",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"gedmo/doctrine-extensions": "^2.4",
|
||||
"jms/serializer-bundle": "^2.4",
|
||||
"kevinpapst/adminlte-bundle": "~2.1",
|
||||
"kimai/kimai2-composer": "^0.1",
|
||||
"mpdf/mpdf": "^7.1",
|
||||
"nelmio/api-doc-bundle": "^3.2",
|
||||
"ocramius/proxy-manager": "2.1.1",
|
||||
|
||||
189
composer.lock
generated
189
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ad6acff0271ed9ad59d9b4ad54ed59cf",
|
||||
"content-hash": "df4ac341f40ef97bcd3b78174125b1c5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "beberlei/DoctrineExtensions",
|
||||
@@ -60,6 +60,50 @@
|
||||
],
|
||||
"time": "2019-02-04T15:31:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "behat/transliterator",
|
||||
"version": "v1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Behat/Transliterator.git",
|
||||
"reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Behat/Transliterator/zipball/826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c",
|
||||
"reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"chuyskywalker/rolling-curl": "^3.1",
|
||||
"php-yaoi/php-yaoi": "^1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.2-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Behat\\Transliterator": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Artistic-1.0"
|
||||
],
|
||||
"description": "String transliterator",
|
||||
"keywords": [
|
||||
"i18n",
|
||||
"slug",
|
||||
"transliterator"
|
||||
],
|
||||
"time": "2017-04-04T11:38:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dama/doctrine-test-bundle",
|
||||
"version": "v5.0.2",
|
||||
@@ -1848,6 +1892,87 @@
|
||||
],
|
||||
"time": "2018-07-12T10:23:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "gedmo/doctrine-extensions",
|
||||
"version": "v2.4.36",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Atlantic18/DoctrineExtensions.git",
|
||||
"reference": "87c78ff9fd4b90460386f753d95622f6fbbfcb27"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Atlantic18/DoctrineExtensions/zipball/87c78ff9fd4b90460386f753d95622f6fbbfcb27",
|
||||
"reference": "87c78ff9fd4b90460386f753d95622f6fbbfcb27",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"behat/transliterator": "~1.2",
|
||||
"doctrine/common": "~2.4",
|
||||
"php": ">=5.3.2"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/annotations": "<1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/common": ">=2.5.0",
|
||||
"doctrine/mongodb-odm": ">=1.0.2",
|
||||
"doctrine/orm": ">=2.5.0",
|
||||
"phpunit/phpunit": "^4.8.35|^5.7|^6.5",
|
||||
"symfony/yaml": "~2.6|~3.0|~4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM",
|
||||
"doctrine/orm": "to use the extensions with the ORM"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Gedmo\\": "lib/Gedmo"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Buchmann",
|
||||
"email": "david@liip.ch"
|
||||
},
|
||||
{
|
||||
"name": "Gediminas Morkevicius",
|
||||
"email": "gediminas.morkevicius@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Gustavo Falco",
|
||||
"email": "comfortablynumb84@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Doctrine2 behavioral extensions",
|
||||
"homepage": "http://gediminasm.org/",
|
||||
"keywords": [
|
||||
"Blameable",
|
||||
"behaviors",
|
||||
"doctrine2",
|
||||
"extensions",
|
||||
"gedmo",
|
||||
"loggable",
|
||||
"nestedset",
|
||||
"sluggable",
|
||||
"sortable",
|
||||
"timestampable",
|
||||
"translatable",
|
||||
"tree",
|
||||
"uploadable"
|
||||
],
|
||||
"time": "2018-07-26T12:16:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jdorn/sql-formatter",
|
||||
"version": "v1.2.17",
|
||||
@@ -2148,16 +2273,16 @@
|
||||
},
|
||||
{
|
||||
"name": "kevinpapst/adminlte-bundle",
|
||||
"version": "2.2",
|
||||
"version": "2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
|
||||
"reference": "f30230e476eec0b22d1f850b6902488d14e0b95e"
|
||||
"reference": "0d095f9ee7ec1450100f65ea392b938296e15863"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/f30230e476eec0b22d1f850b6902488d14e0b95e",
|
||||
"reference": "f30230e476eec0b22d1f850b6902488d14e0b95e",
|
||||
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/0d095f9ee7ec1450100f65ea392b938296e15863",
|
||||
"reference": "0d095f9ee7ec1450100f65ea392b938296e15863",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2205,7 +2330,59 @@
|
||||
}
|
||||
],
|
||||
"description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support",
|
||||
"time": "2019-01-20T16:36:22+00:00"
|
||||
"time": "2019-03-05T20:06:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "kimai/kimai2-composer",
|
||||
"version": "0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kimai/kimai2-composer.git",
|
||||
"reference": "7ff68f4270d2e48920309a3c1530f4d247a24905"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kimai/kimai2-composer/zipball/7ff68f4270d2e48920309a3c1530f4d247a24905",
|
||||
"reference": "7ff68f4270d2e48920309a3c1530f4d247a24905",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^1.0",
|
||||
"composer/semver": "^1.0",
|
||||
"friendsofphp/php-cs-fixer": "^2.10"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "Kimai2\\Composer\\Plugin"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Kimai2\\Composer\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
"email": "kpapst@gmx.net",
|
||||
"homepage": "https://github.com/kevinpapst"
|
||||
}
|
||||
],
|
||||
"description": "An installer for Kimai 2 plugins",
|
||||
"homepage": "https://github.com/kimai/kimai2-composer",
|
||||
"keywords": [
|
||||
"Kimai",
|
||||
"installer",
|
||||
"time-tracker",
|
||||
"time-tracking"
|
||||
],
|
||||
"time": "2019-03-10T19:34:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
|
||||
@@ -31,3 +31,8 @@ doctrine:
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: Kimai
|
||||
loggable:
|
||||
type: annotation
|
||||
alias: Gedmo
|
||||
prefix: Gedmo\Loggable\Entity
|
||||
dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
app.test.activity_recent:
|
||||
path: /{_locale}/activities/recent
|
||||
controller: App\Controller\ActivityController::recentActivitiesAction
|
||||
controller: App\Controller\NavbarController::recentActivitiesAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
App\Entity\Customer:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixedRate, hourlyRate]
|
||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
@@ -41,7 +41,7 @@ App\Entity\Customer:
|
||||
mobile:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
mail:
|
||||
email:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
homepage:
|
||||
|
||||
@@ -51,7 +51,7 @@ services:
|
||||
arguments:
|
||||
$timesheetAsMarkdown: "%kimai.timesheet.markdown%"
|
||||
|
||||
App\Controller\Admin\CustomerController:
|
||||
App\Controller\CustomerController:
|
||||
arguments:
|
||||
$defaults: "%kimai.defaults%"
|
||||
|
||||
@@ -75,8 +75,7 @@ services:
|
||||
class: App\Doctrine\TimesheetSubscriber
|
||||
arguments: [!tagged timesheet.calculator]
|
||||
tags:
|
||||
- { name: doctrine.event_listener, event: prePersist, lazy: true }
|
||||
- { name: doctrine.event_listener, event: preUpdate, lazy: true }
|
||||
- { name: doctrine.event_subscriber, priority: 50 }
|
||||
|
||||
# make sure, that sqlite supports foreign keys and cascade deletes
|
||||
App\Doctrine\SqliteSessionInitSubscriber:
|
||||
@@ -177,4 +176,3 @@ services:
|
||||
App\Repository\InvoiceDocumentRepository:
|
||||
class: App\Repository\InvoiceDocumentRepository
|
||||
arguments: ['%kimai.invoice.documents%']
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\DBAL\Configuration;
|
||||
use Doctrine\DBAL\Connection;
|
||||
@@ -177,18 +178,9 @@ class KimaiImporterCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-load all data to make sure we can fully import everything
|
||||
$users = null;
|
||||
$customer = null;
|
||||
$projects = null;
|
||||
$activities = null;
|
||||
$records = null;
|
||||
$activityToProject = null;
|
||||
$fixedRates = null;
|
||||
$rates = null;
|
||||
|
||||
$bytesStart = memory_get_usage(true);
|
||||
|
||||
// pre-load all data to make sure we can fully import everything
|
||||
try {
|
||||
$users = $this->fetchAllFromImport('users');
|
||||
} catch (\Exception $ex) {
|
||||
@@ -965,7 +957,7 @@ class KimaiImporterCommand extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
$duration = $oldRecord['end'] - $oldRecord['start'];
|
||||
$duration = (int) ($oldRecord['end'] - $oldRecord['start']);
|
||||
|
||||
// ----------------------- unknown user, damned missing data integrity in Kimai v1 -----------------------
|
||||
if (!isset($this->users[$oldRecord['userID']])) {
|
||||
@@ -1021,9 +1013,9 @@ class KimaiImporterCommand extends Command
|
||||
if ($timesheet->getFixedRate() !== null) {
|
||||
$timesheet->setRate($timesheet->getFixedRate());
|
||||
} elseif ($timesheet->getHourlyRate() !== null) {
|
||||
$hourlyRate = $timesheet->getHourlyRate();
|
||||
$rate = (float) $hourlyRate * ($duration / 3600);
|
||||
$timesheet->setRate(round($rate, 2));
|
||||
$hourlyRate = (float) $timesheet->getHourlyRate();
|
||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
||||
$timesheet->setRate($rate);
|
||||
}
|
||||
|
||||
$user = $this->users[$oldRecord['userID']];
|
||||
|
||||
@@ -7,12 +7,10 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use App\Controller\AbstractController;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -38,11 +36,10 @@ class AboutController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="", name="about", methods={"GET"})
|
||||
|
||||
* @param Request $request
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
public function indexAction()
|
||||
{
|
||||
$phpInfo = $this->getPhpInfo();
|
||||
unset($phpInfo[0]);
|
||||
@@ -74,7 +71,7 @@ class AboutController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('admin/system.html.twig', [
|
||||
return $this->render('about/system.html.twig', [
|
||||
'modules' => get_loaded_extensions(),
|
||||
'dotenv' => [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
@@ -10,19 +10,28 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage activity contents in the public part of the site.
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @return ActivityRepository
|
||||
* @return \App\Repository\ActivityRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
@@ -30,18 +39,190 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The flyout to render recent activities and quick-start new recordings.
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*
|
||||
* @return Response
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function recentActivitiesAction()
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$entries = $this->getRepository()->getRecentActivities($user, new \DateTime('-1 year'));
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setOrderBy('name')
|
||||
->setExclusiveVisibility(true)
|
||||
->setPage($page)
|
||||
;
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ActivityQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_activity')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
return $this->renderActivityForm(new Activity(), $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Activity $activity, Request $request)
|
||||
{
|
||||
return $this->renderActivityForm($activity, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setProject($activity->getProject())
|
||||
->setOrderGlobalsFirst(true)
|
||||
->addIgnoredEntity($activity)
|
||||
->setGlobalsOnly(null === $activity->getProject())
|
||||
;
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_activity_delete', ['id' => $activity->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
try {
|
||||
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'navbar/recent-activities.html.twig',
|
||||
['entries' => $entries]
|
||||
'activity/delete.html.twig',
|
||||
[
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderActivityForm(Activity $activity, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($activity);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newActivity = new Activity();
|
||||
$newActivity->setProject($activity->getProject());
|
||||
$editForm = $this->createEditForm($newActivity);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$activity = $newActivity;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'activity/edit.html.twig',
|
||||
[
|
||||
'activity' => $activity,
|
||||
'form' => $editForm->createView()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ActivityQuery $query)
|
||||
{
|
||||
return $this->createForm(ActivityToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_activity', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
{
|
||||
if ($activity->getId() === null) {
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
} else {
|
||||
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
return $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'action' => $url,
|
||||
'method' => 'POST'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Activity;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @return \App\Repository\ActivityRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->getDoctrine()->getRepository(Activity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setOrderBy('name')
|
||||
->setExclusiveVisibility(true)
|
||||
->setPage($page)
|
||||
;
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ActivityQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/activity.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_activity')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
return $this->renderActivityForm(new Activity(), $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Activity $activity, Request $request)
|
||||
{
|
||||
return $this->renderActivityForm($activity, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setProject($activity->getProject())
|
||||
->setOrderGlobalsFirst(true)
|
||||
->addIgnoredEntity($activity)
|
||||
->setGlobalsOnly(null === $activity->getProject())
|
||||
;
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_activity_delete', ['id' => $activity->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
try {
|
||||
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'admin/activity_delete.html.twig',
|
||||
[
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderActivityForm(Activity $activity, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($activity);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newActivity = new Activity();
|
||||
$newActivity->setProject($activity->getProject());
|
||||
$editForm = $this->createEditForm($newActivity);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$activity = $newActivity;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'admin/activity_edit.html.twig',
|
||||
[
|
||||
'activity' => $activity,
|
||||
'form' => $editForm->createView()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ActivityQuery $query)
|
||||
{
|
||||
return $this->createForm(ActivityToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_activity', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
{
|
||||
if ($activity->getId() === null) {
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
} else {
|
||||
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
return $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'action' => $url,
|
||||
'method' => 'POST'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,8 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Customer;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
@@ -23,7 +22,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
* Controller used to manage customer in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/customer")
|
||||
* @Security("is_granted('view_customer')")
|
||||
@@ -78,7 +77,7 @@ class CustomerController extends AbstractController
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/customer.html.twig', [
|
||||
return $this->render('customer/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
@@ -158,7 +157,7 @@ class CustomerController extends AbstractController
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_delete.html.twig', [
|
||||
return $this->render('customer/delete.html.twig', [
|
||||
'customer' => $customer,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
@@ -186,7 +185,7 @@ class CustomerController extends AbstractController
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_edit.html.twig', [
|
||||
return $this->render('customer/edit.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
60
src/Controller/NavbarController.php
Normal file
60
src/Controller/NavbarController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\ActivityRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Controller used to manage navigation-bar contents.
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class NavbarController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @param ActivityRepository $repository
|
||||
*/
|
||||
public function __construct(ActivityRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActivityRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flyout to render recent activities and quick-start new recordings.
|
||||
*
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function recentActivitiesAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$entries = $this->getRepository()->getRecentActivities($user, new \DateTime('-1 year'));
|
||||
|
||||
return $this->render(
|
||||
'navbar/recent-activities.html.twig',
|
||||
['entries' => $entries]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,8 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Form\ProjectEditForm;
|
||||
@@ -69,7 +68,7 @@ class ProjectController extends AbstractController
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getDoctrine()->getRepository(Project::class)->findByQuery($query);
|
||||
|
||||
return $this->render('admin/project.html.twig', [
|
||||
return $this->render('project/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
@@ -145,7 +144,7 @@ class ProjectController extends AbstractController
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
|
||||
return $this->render('admin/project_delete.html.twig', [
|
||||
return $this->render('project/delete.html.twig', [
|
||||
'project' => $project,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
@@ -181,7 +180,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('admin/project_edit.html.twig', [
|
||||
return $this->render('project/edit.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
@@ -238,7 +238,7 @@ class TimesheetController extends AbstractController
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('timesheet_paginated', ['page' => $request->get('page')]);
|
||||
return $this->redirectToRoute('timesheet_paginated', ['page' => $request->get('page', 1)]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,11 +103,11 @@ trait TimesheetControllerTrait
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($redirectRoute, ['page' => $request->get('page')]);
|
||||
return $this->redirectToRoute($redirectRoute, ['page' => $request->get('page', 1)]);
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
'entry' => $entry,
|
||||
'timesheet' => $entry,
|
||||
'form' => $editForm->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ trait TimesheetControllerTrait
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
'entry' => $entry,
|
||||
'timesheet' => $entry,
|
||||
'form' => $createForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Controller\TimesheetControllerTrait;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
@@ -26,7 +24,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* @Route(path="/team/timesheet")
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends AbstractController
|
||||
class TimesheetTeamController extends AbstractController
|
||||
{
|
||||
use TimesheetControllerTrait;
|
||||
|
||||
@@ -60,7 +58,7 @@ class TimesheetController extends AbstractController
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/timesheet.html.twig', [
|
||||
return $this->render('timesheet-team/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'page' => $query->getPage(),
|
||||
'query' => $query,
|
||||
@@ -101,7 +99,7 @@ class TimesheetController extends AbstractController
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/timesheet_export.html.twig', [
|
||||
return $this->render('timesheet-team/export.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
]);
|
||||
@@ -129,7 +127,7 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
public function editAction(Timesheet $entry, Request $request)
|
||||
{
|
||||
return $this->edit($entry, $request, 'admin_timesheet_paginated', 'admin/timesheet_edit.html.twig');
|
||||
return $this->edit($entry, $request, 'admin_timesheet_paginated', 'timesheet-team/edit.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +139,7 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
return $this->create($request, 'admin_timesheet', 'admin/timesheet_edit.html.twig');
|
||||
return $this->create($request, 'admin_timesheet', 'timesheet-team/edit.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +162,7 @@ class TimesheetController extends AbstractController
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_timesheet_paginated', ['page' => $request->get('page')]);
|
||||
return $this->redirectToRoute('admin_timesheet_paginated', ['page' => $request->get('page', 1)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7,9 +7,8 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Form\Toolbar\UserToolbarForm;
|
||||
@@ -74,7 +73,7 @@ class UserController extends AbstractController
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/user.html.twig', [
|
||||
return $this->render('user/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
@@ -119,7 +118,7 @@ class UserController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'admin/user_edit.html.twig',
|
||||
'user/edit.html.twig',
|
||||
[
|
||||
'user' => $user,
|
||||
'form' => $editForm->createView()
|
||||
@@ -159,7 +158,7 @@ class UserController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'admin/user_delete.html.twig',
|
||||
'user/delete.html.twig',
|
||||
[
|
||||
'user' => $userToDelete,
|
||||
'stats' => $stats,
|
||||
@@ -23,6 +23,8 @@ use Faker\Generator;
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class CustomerFixtures extends Fixture
|
||||
{
|
||||
@@ -32,7 +34,7 @@ class CustomerFixtures extends Fixture
|
||||
public const MAX_BUDGET = 100000;
|
||||
public const MIN_GLOBAL_ACTIVITIES = 5;
|
||||
public const MAX_GLOBAL_ACTIVITIES = 50;
|
||||
public const MIN_PROJECTS_PER_CUSTOMER = 1;
|
||||
public const MIN_PROJECTS_PER_CUSTOMER = 2;
|
||||
public const MAX_PROJECTS_PER_CUSTOMER = 25;
|
||||
public const MIN_ACTIVITIES_PER_PROJECT = 0;
|
||||
public const MAX_ACTIVITIES_PER_PROJECT = 25;
|
||||
@@ -48,6 +50,7 @@ class CustomerFixtures extends Fixture
|
||||
for ($c = 1; $c <= $amountCustomers; $c++) {
|
||||
$visibleCustomer = 0 != $c % 5;
|
||||
$customer = $this->createCustomer($faker, $visibleCustomer);
|
||||
$manager->persist($customer);
|
||||
|
||||
$projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER);
|
||||
for ($p = 1; $p <= $projectForCustomer; $p++) {
|
||||
@@ -63,8 +66,6 @@ class CustomerFixtures extends Fixture
|
||||
}
|
||||
}
|
||||
|
||||
$manager->persist($customer);
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ use Faker\Generator;
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class InvoiceFixtures extends Fixture
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
@@ -25,6 +26,8 @@ use Faker\Factory;
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
@@ -181,11 +184,12 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$end = $end->modify('+ ' . (rand(self::MIN_MINUTES_PER_ENTRY, self::MAX_MINUTES_PER_ENTRY)) . ' minutes');
|
||||
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$rate = $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
||||
|
||||
$entry
|
||||
->setEnd($end)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
->setRate($rate)
|
||||
->setDuration($duration);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class UserFixtures extends Fixture
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Doctrine;
|
||||
|
||||
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration as BaseAbstractMigration;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
@@ -50,6 +51,55 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
return getenv('DATABASE_PREFIX') . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function preUp(Schema $schema): void
|
||||
{
|
||||
$this->abortIfPlatformNotSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function preDown(Schema $schema): void
|
||||
{
|
||||
$this->abortIfPlatformNotSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the migration is the current platform is not supported.
|
||||
*
|
||||
* @throws DBALException
|
||||
*/
|
||||
protected function abortIfPlatformNotSupported()
|
||||
{
|
||||
$platform = $this->getPlatform();
|
||||
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws DBALException
|
||||
*/
|
||||
protected function isPlatformSqlite()
|
||||
{
|
||||
return ($this->getPlatform() === 'sqlite');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws DBALException
|
||||
*/
|
||||
protected function isPlatformMysql()
|
||||
{
|
||||
return ($this->getPlatform() === 'mysql');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws DBALException
|
||||
@@ -84,7 +134,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
protected function addSqlDropIndex($indexName, $tableName)
|
||||
{
|
||||
$dropSql = 'DROP INDEX ' . $indexName;
|
||||
if ($this->getPlatform() === 'mysql') {
|
||||
if (!$this->isPlatformSqlite()) {
|
||||
$dropSql .= ' ON ' . $tableName;
|
||||
}
|
||||
$this->addSql($dropSql);
|
||||
|
||||
@@ -15,6 +15,16 @@ use Doctrine\DBAL\Events;
|
||||
|
||||
class SqliteSessionInitSubscriber implements EventSubscriber
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
Events::postConnect,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ConnectionEventArgs $args
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
@@ -24,14 +34,7 @@ class SqliteSessionInitSubscriber implements EventSubscriber
|
||||
if ('sqlite' !== strtolower($args->getDatabasePlatform()->getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$args->getConnection()->executeUpdate('PRAGMA foreign_keys = ON;');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return [Events::postConnect];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace App\Doctrine;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* A listener to make sure all Timesheet entries will have a proper duration.
|
||||
@@ -26,7 +26,6 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
protected $calculator;
|
||||
|
||||
/**
|
||||
* TimesheetSubscriber constructor.
|
||||
* @param iterable $calculators
|
||||
*/
|
||||
public function __construct(iterable $calculators)
|
||||
@@ -48,38 +47,43 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
'prePersist',
|
||||
'preUpdate',
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PreUpdateEventArgs $args
|
||||
* @param OnFlushEventArgs $args
|
||||
*/
|
||||
public function preUpdate(PreUpdateEventArgs $args)
|
||||
public function onFlush(OnFlushEventArgs $args)
|
||||
{
|
||||
$this->calculateFields($args);
|
||||
}
|
||||
$em = $args->getEntityManager();
|
||||
$uow = $em->getUnitOfWork();
|
||||
$meta = $em->getClassMetadata(Timesheet::class);
|
||||
|
||||
/**
|
||||
* @param LifecycleEventArgs $args
|
||||
*/
|
||||
public function prePersist(LifecycleEventArgs $args)
|
||||
{
|
||||
$this->calculateFields($args);
|
||||
}
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LifecycleEventArgs $args
|
||||
*/
|
||||
protected function calculateFields(LifecycleEventArgs $args)
|
||||
{
|
||||
$entity = $args->getObject();
|
||||
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
return;
|
||||
$this->calculateFields($entity);
|
||||
$uow->recomputeSingleEntityChangeSet($meta, $entity);
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->calculateFields($entity);
|
||||
$uow->recomputeSingleEntityChangeSet($meta, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entity
|
||||
*/
|
||||
protected function calculateFields(Timesheet $entity)
|
||||
{
|
||||
foreach ($this->calculator as $calculator) {
|
||||
$calculator->calculate($entity);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Activity
|
||||
*
|
||||
* @ORM\Table(name="activities")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||
*/
|
||||
@@ -71,7 +69,7 @@ class Activity
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="fixed_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
@@ -79,7 +77,7 @@ class Activity
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="hourly_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
@@ -13,8 +13,6 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Customer
|
||||
*
|
||||
* @ORM\Table(name="customers")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*/
|
||||
@@ -130,9 +128,9 @@ class Customer
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="mail", type="string", length=255, nullable=true)
|
||||
* @ORM\Column(name="email", type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $mail;
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
@@ -152,7 +150,7 @@ class Customer
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="fixed_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
@@ -160,7 +158,7 @@ class Customer
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="hourly_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
@@ -443,9 +441,9 @@ class Customer
|
||||
* @param string $mail
|
||||
* @return Customer
|
||||
*/
|
||||
public function setMail($mail)
|
||||
public function setEmail($mail)
|
||||
{
|
||||
$this->mail = $mail;
|
||||
$this->email = $mail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -455,9 +453,9 @@ class Customer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMail()
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->mail;
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,8 +13,6 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Project
|
||||
*
|
||||
* @ORM\Table(name="projects")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||
*/
|
||||
@@ -73,7 +71,7 @@ class Project
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="budget", type="decimal", precision=10, scale=2, nullable=false)
|
||||
* @ORM\Column(name="budget", type="float", precision=10, scale=2, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $budget = 0.00;
|
||||
@@ -88,7 +86,7 @@ class Project
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="fixed_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
@@ -96,7 +94,7 @@ class Project
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="hourly_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
@@ -13,8 +13,6 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Timesheet entity.
|
||||
*
|
||||
* @ORM\Table(
|
||||
* name="timesheet",
|
||||
* indexes={
|
||||
@@ -109,7 +107,7 @@ class Timesheet
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="rate", type="decimal", precision=10, scale=2, nullable=false)
|
||||
* @ORM\Column(name="rate", type="float", precision=10, scale=2, nullable=false)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
@@ -117,7 +115,7 @@ class Timesheet
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="fixed_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
@@ -125,7 +123,7 @@ class Timesheet
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @ORM\Column(name="hourly_rate", type="float", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
@@ -211,7 +209,7 @@ class Timesheet
|
||||
|
||||
if (null === $end) {
|
||||
$this->duration = 0;
|
||||
$this->rate = 0;
|
||||
$this->rate = 0.00;
|
||||
} else {
|
||||
$this->timezone = $end->getTimezone()->getName();
|
||||
}
|
||||
@@ -298,8 +296,6 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description
|
||||
*
|
||||
* @param string $description
|
||||
* @return Timesheet
|
||||
*/
|
||||
@@ -311,8 +307,6 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
@@ -321,8 +315,6 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* Set rate
|
||||
*
|
||||
* @param float $rate
|
||||
* @return Timesheet
|
||||
*/
|
||||
@@ -334,8 +326,6 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getRate()
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ThemeEvent extends Event
|
||||
{
|
||||
@@ -23,23 +22,22 @@ class ThemeEvent extends Event
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
protected $request;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $content = '';
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $payload = null;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param User $user
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct(Request $request, User $user)
|
||||
public function __construct(User $user, $payload = null)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->user = $user;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,14 +48,6 @@ class ThemeEvent extends Event
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -76,4 +66,22 @@ class ThemeEvent extends Event
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPayload()
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function setPayload($payload)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.activity'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourly_rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixed_rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
|
||||
|
||||
@@ -96,12 +96,12 @@ class ActivityEditForm extends AbstractType
|
||||
|
||||
$builder
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'label' => 'label.fixedRate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'label' => 'label.hourlyRate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
|
||||
@@ -42,7 +42,7 @@ class CustomerEditForm extends AbstractType
|
||||
'label' => 'label.name',
|
||||
])
|
||||
->add('number', TextType::class, [
|
||||
'label' => 'label.customer_number',
|
||||
'label' => 'label.number',
|
||||
'required' => false,
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
@@ -81,7 +81,7 @@ class CustomerEditForm extends AbstractType
|
||||
'required' => false,
|
||||
'attr' => ['icon' => 'mobile'],
|
||||
])
|
||||
->add('mail', EmailType::class, [
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'label.email',
|
||||
'required' => false,
|
||||
])
|
||||
@@ -93,12 +93,12 @@ class CustomerEditForm extends AbstractType
|
||||
'label' => 'label.timezone',
|
||||
])
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'label' => 'label.fixedRate',
|
||||
'required' => false,
|
||||
'currency' => $customer->getCurrency() ?? false,
|
||||
])
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'label' => 'label.hourlyRate',
|
||||
'required' => false,
|
||||
'currency' => $customer->getCurrency() ?? false,
|
||||
])
|
||||
|
||||
@@ -52,7 +52,7 @@ class ProjectEditForm extends AbstractType
|
||||
'required' => false,
|
||||
])
|
||||
->add('orderNumber', TextType::class, [
|
||||
'label' => 'label.order_number',
|
||||
'label' => 'label.orderNumber',
|
||||
'required' => false,
|
||||
])
|
||||
->add('customer', CustomerType::class, [
|
||||
@@ -62,12 +62,12 @@ class ProjectEditForm extends AbstractType
|
||||
},
|
||||
])
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'label' => 'label.fixedRate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'label' => 'label.hourlyRate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
|
||||
@@ -265,12 +265,12 @@ class TimesheetEditForm extends AbstractType
|
||||
if ($options['include_rate']) {
|
||||
$builder
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'label' => 'label.fixedRate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'label' => 'label.hourlyRate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
|
||||
@@ -16,6 +16,8 @@ use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Adds the exported column to the timesheet table
|
||||
*
|
||||
* @version 0.8
|
||||
*/
|
||||
final class Version20190124004014 extends AbstractMigration
|
||||
{
|
||||
|
||||
@@ -17,6 +17,8 @@ use Doctrine\DBAL\Schema\Schema;
|
||||
/**
|
||||
* Adds the timezone column to the timesheet table
|
||||
* See https://github.com/kevinpapst/kimai2/pull/372 for further information.
|
||||
*
|
||||
* @version 0.8
|
||||
*/
|
||||
final class Version20190201150324 extends AbstractMigration
|
||||
{
|
||||
|
||||
@@ -16,6 +16,8 @@ use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Cleanup the user_preferences table from old configs.
|
||||
*
|
||||
* @version 0.9
|
||||
*/
|
||||
final class Version20190219200020 extends AbstractMigration
|
||||
{
|
||||
|
||||
141
src/Migrations/Version20190305152308.php
Normal file
141
src/Migrations/Version20190305152308.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* - rename mail to email in customer table
|
||||
* - introducing foreign keys in SQLite tables
|
||||
* - converts all decimal to float values, as decimals are treated as string in PHP:
|
||||
* https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#decimal
|
||||
*
|
||||
* @version 0.9
|
||||
*/
|
||||
final class Version20190305152308 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$customers = $this->getTableName('customers');
|
||||
$projects = $this->getTableName('projects');
|
||||
$activities = $this->getTableName('activities');
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
$users = $this->getTableName('users');
|
||||
|
||||
if ($this->isPlatformSqlite()) {
|
||||
// first backup of ALL tables
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_timesheet AS SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM ' . $timesheet);
|
||||
|
||||
$this->addSql('DROP INDEX IDX_8811FE1C166D1F9C');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_activities AS SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM ' . $activities);
|
||||
|
||||
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_projects AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects);
|
||||
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_customers AS SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixed_rate, hourly_rate FROM ' . $customers);
|
||||
|
||||
// now we can drop and re-create the tables
|
||||
$this->addSql('DROP TABLE ' . $customers);
|
||||
$this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, number VARCHAR(50) DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL COLLATE BINARY, contact VARCHAR(255) DEFAULT NULL COLLATE BINARY, address CLOB DEFAULT NULL COLLATE BINARY, country VARCHAR(2) NOT NULL COLLATE BINARY, currency VARCHAR(3) NOT NULL COLLATE BINARY, phone VARCHAR(255) DEFAULT NULL COLLATE BINARY, fax VARCHAR(255) DEFAULT NULL COLLATE BINARY, mobile VARCHAR(255) DEFAULT NULL COLLATE BINARY, email VARCHAR(255) DEFAULT NULL COLLATE BINARY, homepage VARCHAR(255) DEFAULT NULL COLLATE BINARY, timezone VARCHAR(255) NOT NULL COLLATE BINARY, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $customers . ' (id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixed_rate, hourly_rate) SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixed_rate, hourly_rate FROM __temp__kimai2_customers');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_customers');
|
||||
|
||||
$this->addSql('DROP TABLE ' . $projects);
|
||||
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, order_number CLOB DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, budget DOUBLE PRECISION NOT NULL, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL, CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__kimai2_projects');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_projects');
|
||||
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
|
||||
|
||||
$this->addSql('DROP TABLE ' . $activities);
|
||||
$this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL, CONSTRAINT FK_8811FE1C166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO ' . $activities . ' (id, project_id, name, comment, visible, fixed_rate, hourly_rate) SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM __temp__kimai2_activities');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_activities');
|
||||
$this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activities . ' (project_id)');
|
||||
|
||||
$this->addSql('DROP TABLE ' . $timesheet);
|
||||
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL --(DC2Type:datetime)
|
||||
, timezone VARCHAR(64) NOT NULL COLLATE BINARY, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, exported BOOLEAN NOT NULL, end_time DATETIME DEFAULT NULL --(DC2Type:datetime)
|
||||
, rate DOUBLE PRECISION NOT NULL, fixed_rate DOUBLE PRECISION DEFAULT NULL, hourly_rate DOUBLE PRECISION DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported) SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM __temp__kimai2_timesheet');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_timesheet');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $customers . ' CHANGE mail email VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate DOUBLE PRECISION NOT NULL, CHANGE fixed_rate fixed_rate DOUBLE PRECISION DEFAULT NULL, CHANGE hourly_rate hourly_rate DOUBLE PRECISION DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$customers = $this->getTableName('customers');
|
||||
$projects = $this->getTableName('projects');
|
||||
$activities = $this->getTableName('activities');
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
|
||||
if ($this->isPlatformSqlite()) {
|
||||
// first backup of ALL tables
|
||||
$this->addSql('DROP INDEX IDX_8811FE1C166D1F9C');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_activities AS SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM ' . $activities);
|
||||
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_customers AS SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixed_rate, hourly_rate FROM ' . $customers);
|
||||
|
||||
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_projects AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects);
|
||||
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__kimai2_timesheet AS SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM ' . $timesheet);
|
||||
|
||||
// now we can drop and re-create the tables
|
||||
$this->addSql('DROP TABLE ' . $activities);
|
||||
$this->addSql('CREATE TABLE ' . $activities . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $activities . ' (id, project_id, name, comment, visible, fixed_rate, hourly_rate) SELECT id, project_id, name, comment, visible, fixed_rate, hourly_rate FROM __temp__kimai2_activities');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_activities');
|
||||
$this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activities . ' (project_id)');
|
||||
|
||||
$this->addSql('DROP TABLE ' . $customers);
|
||||
$this->addSql('CREATE TABLE ' . $customers . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $customers . ' (id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixed_rate, hourly_rate) SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixed_rate, hourly_rate FROM __temp__kimai2_customers');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_customers');
|
||||
|
||||
$this->addSql('DROP TABLE ' . $projects);
|
||||
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__kimai2_projects');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_projects');
|
||||
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
|
||||
|
||||
$this->addSql('DROP TABLE ' . $timesheet);
|
||||
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL --(DC2Type:datetime)
|
||||
, timezone VARCHAR(64) NOT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, exported BOOLEAN NOT NULL, end_time DATETIME DEFAULT NULL --(DC2Type:datetime)
|
||||
, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported) SELECT id, user, activity_id, project_id, start_time, end_time, timezone, duration, description, rate, fixed_rate, hourly_rate, exported FROM __temp__kimai2_timesheet');
|
||||
$this->addSql('DROP TABLE __temp__kimai2_timesheet');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE ' . $activities . ' CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $customers . ' CHANGE email mail VARCHAR(255) DEFAULT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE budget budget NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE rate rate NUMERIC(10, 2) NOT NULL, CHANGE fixed_rate fixed_rate NUMERIC(10, 2) DEFAULT NULL, CHANGE hourly_rate hourly_rate NUMERIC(10, 2) DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,14 +53,14 @@ class ActivityQuery extends ProjectQuery
|
||||
*/
|
||||
public function isGlobalsOnly(): bool
|
||||
{
|
||||
return $this->globalsOnly;
|
||||
return (bool) $this->globalsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $globalsOnly
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function setGlobalsOnly(bool $globalsOnly)
|
||||
public function setGlobalsOnly($globalsOnly)
|
||||
{
|
||||
$this->globalsOnly = $globalsOnly;
|
||||
|
||||
|
||||
@@ -82,10 +82,10 @@ class TimesheetRepository extends AbstractRepository
|
||||
switch ($type) {
|
||||
case self::STATS_QUERY_ACTIVE:
|
||||
return count($this->getActiveEntries($user));
|
||||
break;
|
||||
|
||||
case self::STATS_QUERY_MONTHLY:
|
||||
return $this->getMonthlyStats($user, $begin, $end);
|
||||
break;
|
||||
|
||||
case self::STATS_QUERY_DURATION:
|
||||
$what = 'SUM(t.duration)';
|
||||
break;
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Timesheet\Calculator;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use App\Timesheet\Util;
|
||||
|
||||
/**
|
||||
* Implementation to calculate the rate for a timesheet record.
|
||||
@@ -54,8 +55,8 @@ class RateCalculator implements CalculatorInterface
|
||||
$hourlyRate = $this->findHourlyRate($record);
|
||||
$factor = $this->getRateFactor($record);
|
||||
|
||||
$hourlyRate = (float) $hourlyRate * $factor;
|
||||
$rate = (float) $hourlyRate * ($record->getDuration() / 3600);
|
||||
$hourlyRate = (float) ($hourlyRate * $factor);
|
||||
$rate = Util::calculateRate($hourlyRate, $record->getDuration());
|
||||
|
||||
$record->setHourlyRate($hourlyRate);
|
||||
$record->setRate($rate);
|
||||
|
||||
31
src/Timesheet/Util.php
Normal file
31
src/Timesheet/Util.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Timesheet;
|
||||
|
||||
/**
|
||||
* A static helper class for re-usable functionality.
|
||||
*/
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* Calculates the rate for a hourly rate and a given duration in seconds.
|
||||
*
|
||||
* @param float $hourlyRate
|
||||
* @param int $seconds
|
||||
* @return float
|
||||
*/
|
||||
public static function calculateRate(float $hourlyRate, int $seconds): float
|
||||
{
|
||||
$rate = (float) ($hourlyRate * ($seconds / 3600));
|
||||
$rate = round($rate, 2);
|
||||
|
||||
return $rate;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,13 @@ namespace App\Twig;
|
||||
|
||||
use App\Utils\LocaleSettings;
|
||||
use DateTime;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* Date specific twig extensions
|
||||
*/
|
||||
class DateExtensions extends \Twig_Extension
|
||||
class DateExtensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var LocaleSettings|null
|
||||
@@ -30,6 +31,10 @@ class DateExtensions extends \Twig_Extension
|
||||
* @var string
|
||||
*/
|
||||
protected $dateTimeFormat = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $dateTimeTypeFormat = null;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@@ -56,6 +61,7 @@ class DateExtensions extends \Twig_Extension
|
||||
new TwigFilter('month_name', [$this, 'monthName']),
|
||||
new TwigFilter('date_short', [$this, 'dateShort']),
|
||||
new TwigFilter('date_time', [$this, 'dateTime']),
|
||||
new TwigFilter('date_full', [$this, 'dateTimeFull']),
|
||||
new TwigFilter('date_format', [$this, 'dateFormat']),
|
||||
new TwigFilter('time', [$this, 'time']),
|
||||
new TwigFilter('hour24', [$this, 'hour24']),
|
||||
@@ -88,6 +94,28 @@ class DateExtensions extends \Twig_Extension
|
||||
return date_format($date, $this->dateTimeFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $date
|
||||
* @return string
|
||||
*/
|
||||
public function dateTimeFull(DateTime $date)
|
||||
{
|
||||
if (null === $this->dateTimeTypeFormat) {
|
||||
$this->dateTimeTypeFormat = $this->localeSettings->getDateTimeTypeFormat();
|
||||
}
|
||||
|
||||
$formatter = new \IntlDateFormatter(
|
||||
$this->localeSettings->getLocale(),
|
||||
\IntlDateFormatter::MEDIUM,
|
||||
\IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
\IntlDateFormatter::GREGORIAN,
|
||||
$this->dateTimeTypeFormat
|
||||
);
|
||||
|
||||
return $formatter->format($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $date
|
||||
* @param string $format
|
||||
|
||||
163
src/Twig/EntityExtensions.php
Normal file
163
src/Twig/EntityExtensions.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* Entity specific twig extensions.
|
||||
* Should be used with caution, as they can trigger a lot of DB queries.
|
||||
*/
|
||||
class EntityExtensions extends AbstractExtension
|
||||
{
|
||||
private const UNKNOWN_NAME = '-unknown-';
|
||||
|
||||
/**
|
||||
* @var UserRepository|null
|
||||
*/
|
||||
private $users = null;
|
||||
/**
|
||||
* @var CustomerRepository|null
|
||||
*/
|
||||
private $customers = null;
|
||||
/**
|
||||
* @var ProjectRepository|null
|
||||
*/
|
||||
private $projects = null;
|
||||
/**
|
||||
* @var ActivityRepository|null
|
||||
*/
|
||||
private $activities = null;
|
||||
|
||||
/**
|
||||
* @param UserRepository $users
|
||||
* @param CustomerRepository $customers
|
||||
* @param ProjectRepository $projects
|
||||
* @param ActivityRepository $activities
|
||||
*/
|
||||
public function __construct(UserRepository $users, CustomerRepository $customers, ProjectRepository $projects, ActivityRepository $activities)
|
||||
{
|
||||
$this->users = $users;
|
||||
$this->customers = $customers;
|
||||
$this->projects = $projects;
|
||||
$this->activities = $activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('user', [$this, 'getUser']),
|
||||
new TwigFilter('customer', [$this, 'getCustomer']),
|
||||
new TwigFilter('project', [$this, 'getProject']),
|
||||
new TwigFilter('activity', [$this, 'getActivity']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|User $user
|
||||
* @param bool $allowEmpty
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser($user, $allowEmpty = true)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$entity = $this->users->getById($user);
|
||||
|
||||
if (null === $entity) {
|
||||
$entity = $this->users->loadUserByUsername($user);
|
||||
}
|
||||
|
||||
if (null === $entity && false === $allowEmpty) {
|
||||
$entity = new User();
|
||||
$entity->setUsername(self::UNKNOWN_NAME);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|Customer $customer
|
||||
* @param bool $allowEmpty
|
||||
* @return Customer|null
|
||||
*/
|
||||
public function getCustomer($customer, $allowEmpty = true)
|
||||
{
|
||||
if ($customer instanceof Customer) {
|
||||
return $customer;
|
||||
}
|
||||
|
||||
$entity = $this->customers->getById($customer);
|
||||
|
||||
if (null === $entity && false === $allowEmpty) {
|
||||
$entity = new Customer();
|
||||
$entity->setName(self::UNKNOWN_NAME);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|Project $project
|
||||
* @param bool $allowEmpty
|
||||
* @return Project|null
|
||||
*/
|
||||
public function getProject($project, $allowEmpty = true)
|
||||
{
|
||||
if ($project instanceof Project) {
|
||||
return $project;
|
||||
}
|
||||
|
||||
$entity = $this->projects->getById($project);
|
||||
|
||||
if (null === $entity && false === $allowEmpty) {
|
||||
$entity = new Project();
|
||||
$entity->setName(self::UNKNOWN_NAME);
|
||||
$entity->setCustomer((new Customer())->setName(self::UNKNOWN_NAME));
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|Activity $activity
|
||||
* @param bool $allowEmpty
|
||||
* @return Activity|null
|
||||
*/
|
||||
public function getActivity($activity, $allowEmpty = true)
|
||||
{
|
||||
if ($activity instanceof Activity) {
|
||||
return $activity;
|
||||
}
|
||||
|
||||
$entity = $this->activities->getById($activity);
|
||||
|
||||
if (null === $entity && false === $allowEmpty) {
|
||||
$entity = new Activity();
|
||||
$entity->setName(self::UNKNOWN_NAME);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,43 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\ThemeEvent;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use App\Security\CurrentUser;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class EventController extends Controller
|
||||
class EventExtensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, CurrentUser $user)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->user = $user->getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('trigger', [$this, 'triggerEvent']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,19 +65,19 @@ class EventController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $eventName
|
||||
* @return ThemeEvent|Response
|
||||
* @param mixed $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function trigger(Request $request, string $event)
|
||||
public function triggerEvent(string $eventName, $payload = null)
|
||||
{
|
||||
if (!$this->hasListener($event)) {
|
||||
return new Response();
|
||||
$themeEvent = new ThemeEvent($this->user, $payload);
|
||||
|
||||
if ($this->hasListener($eventName)) {
|
||||
$this->getDispatcher()->dispatch($eventName, $themeEvent);
|
||||
}
|
||||
|
||||
$themeEvent = new ThemeEvent($request, $this->getUser());
|
||||
$this->getDispatcher()->dispatch($event, $themeEvent);
|
||||
|
||||
return new Response($themeEvent->getContent());
|
||||
return $themeEvent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,12 +16,14 @@ use App\Utils\LocaleSettings;
|
||||
use NumberFormatter;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Intl\Intl;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* Multiple Twig extensions: filters and functions
|
||||
*/
|
||||
class Extensions extends \Twig_Extension
|
||||
class Extensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var LocaleSettings
|
||||
@@ -98,6 +100,7 @@ class Extensions extends \Twig_Extension
|
||||
'xlsx' => 'fas fa-file-excel',
|
||||
'on' => 'fas fa-toggle-on',
|
||||
'off' => 'fas fa-toggle-off',
|
||||
'audit' => 'fas fa-history',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -132,12 +135,26 @@ class Extensions extends \Twig_Extension
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new \Twig_SimpleFunction('locales', [$this, 'getLocales']),
|
||||
new \Twig_SimpleFunction('is_visible_column', [$this, 'isColumnVisible']),
|
||||
new \Twig_SimpleFunction('is_datatable_configured', [$this, 'isDatatableConfigured']),
|
||||
new TwigFunction('locales', [$this, 'getLocales']),
|
||||
new TwigFunction('is_visible_column', [$this, 'isColumnVisible']),
|
||||
new TwigFunction('is_datatable_configured', [$this, 'isDatatableConfigured']),
|
||||
new TwigFunction('class_name', [$this, 'getClassName']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $object
|
||||
* @return null|string
|
||||
*/
|
||||
public function getClassName($object)
|
||||
{
|
||||
if (!is_object($object)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_class($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dataTable
|
||||
* @param string $size
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Utils\Markdown;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* A twig extension to handle markdown parser.
|
||||
*/
|
||||
class MarkdownExtension extends \Twig_Extension
|
||||
class MarkdownExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var Markdown
|
||||
|
||||
@@ -152,7 +152,7 @@ class LocaleSettings
|
||||
*/
|
||||
public function isTwentyFourHours(?string $locale = null): bool
|
||||
{
|
||||
return $this->getConfigByLocaleAndKey('24_hours', $locale);
|
||||
return (bool) $this->getConfigByLocaleAndKey('24_hours', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
@@ -59,7 +59,7 @@ class TimesheetValidator extends ConstraintValidator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $value
|
||||
* @param TimesheetEntity $value
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
@@ -68,7 +68,7 @@ class TimesheetValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\Timesheet');
|
||||
}
|
||||
|
||||
if (!is_object($value) || !($value instanceof Timesheet)) {
|
||||
if (!is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,10 +78,10 @@ class TimesheetValidator extends ConstraintValidator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validatePermissions(Timesheet $timesheet, ExecutionContextInterface $context)
|
||||
protected function validatePermissions(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
// special case that would otherwise need to be validated in several controllers:
|
||||
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
|
||||
@@ -102,10 +102,10 @@ class TimesheetValidator extends ConstraintValidator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateBeginAndEnd(Timesheet $timesheet, ExecutionContextInterface $context)
|
||||
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$context->buildViolation('You must submit a begin date.')
|
||||
@@ -135,10 +135,10 @@ class TimesheetValidator extends ConstraintValidator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActivityAndProject(Timesheet $timesheet, ExecutionContextInterface $context)
|
||||
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
if (null === ($activity = $timesheet->getActivity())) {
|
||||
$context->buildViolation('A timesheet must have an activity.')
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"beberlei/doctrineextensions": {
|
||||
"version": "v1.2.0"
|
||||
},
|
||||
"behat/transliterator": {
|
||||
"version": "v1.2.0"
|
||||
},
|
||||
"composer/semver": {
|
||||
"version": "1.4.2"
|
||||
},
|
||||
@@ -137,6 +140,9 @@
|
||||
"fzaninotto/faker": {
|
||||
"version": "v1.8.0"
|
||||
},
|
||||
"gedmo/doctrine-extensions": {
|
||||
"version": "v2.4.36"
|
||||
},
|
||||
"jdorn/sql-formatter": {
|
||||
"version": "v1.2.17"
|
||||
},
|
||||
@@ -161,6 +167,9 @@
|
||||
"kevinpapst/adminlte-bundle": {
|
||||
"version": "0.3"
|
||||
},
|
||||
"kimai/kimai2-composer": {
|
||||
"version": "0.1"
|
||||
},
|
||||
"markbaker/complex": {
|
||||
"version": "1.4.6"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_activity.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.activity(activity, 'delete') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_activity.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.activity(activity, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
@@ -2,16 +2,11 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_activity.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseActivityAdmin', 'visibility': '#modal_activity_admin'} %}
|
||||
{% if is_granted('create_activity') %}
|
||||
{% set actions = actions|merge({'create': path('admin_activity_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
{% block page_actions %}{{ actions.activities('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseActivityAdmin', showFilter) }}
|
||||
@@ -55,14 +50,7 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('admin_activity_edit', {'id': entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_activity_delete', {'id': entry.id})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
{{ actions.activity(entry, 'index') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1,12 +0,0 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
|
||||
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': (entry.id ? 'timesheet.edit'|trans : 'create'|trans),
|
||||
'form': form,
|
||||
'back': path('admin_timesheet')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
{% block avanzu_navbar_notifications %}
|
||||
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
|
||||
{{ render(controller('App\\Controller\\ActivityController::recentActivitiesAction')) }}
|
||||
{{ render(controller('App\\Controller\\NavbarController::recentActivitiesAction')) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -95,14 +95,16 @@
|
||||
{% block stylesheets %}
|
||||
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
|
||||
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
|
||||
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::STYLESHEET')})) }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
<script src="{{ asset('build/app.js') }}"></script>
|
||||
{% include 'partials/head.html.twig' %}
|
||||
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::HTML_HEAD')})) }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::HTML_HEAD')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
@@ -128,5 +130,6 @@
|
||||
{# $.kimai.pauseRecord('li.messages-menu ul.menu li'); #}
|
||||
});
|
||||
</script>
|
||||
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::JAVASCRIPT')})) }}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.customer(customer, 'delete') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.customer(customer, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
@@ -2,16 +2,11 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseCustomerAdmin', 'visibility': '#modal_customer_admin'} %}
|
||||
{% if is_granted('create_customer') %}
|
||||
{% set actions = actions|merge({'create': path('admin_customer_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
{% block page_actions %}{{ actions.customers('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseCustomerAdmin', showFilter) }}
|
||||
@@ -26,7 +21,7 @@
|
||||
'name': 'alwaysVisible',
|
||||
'comment': 'hidden-xs',
|
||||
'country': 'hidden-xs',
|
||||
'customer_number': 'hidden-xs',
|
||||
'number': 'hidden-xs',
|
||||
'currency': 'hidden-xs',
|
||||
'visible': 'hidden-xs',
|
||||
'actions': 'alwaysVisible',
|
||||
@@ -41,21 +36,11 @@
|
||||
<td>{{ entry.name }} {% if entry.company is not empty %}({{ entry.company }}){% endif %}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer_number') }}">{{ entry.number }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'currency') }}">{{ entry.currency }} {{ entry.currency|currency }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('admin_customer_edit', {'id': entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_project') %}
|
||||
{% set actionButtons = actionButtons|merge({'project': path('admin_project', {'customer': entry.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_customer_delete', {'id': entry.id})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
{{ actions.customer(entry, 'index') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -91,8 +91,8 @@
|
||||
'label.activity',
|
||||
'label.description',
|
||||
'label.exported',
|
||||
'label.hourly_rate',
|
||||
'label.fixed_rate',
|
||||
'label.hourlyRate',
|
||||
'label.fixedRate',
|
||||
'label.duration',
|
||||
'label.rate',
|
||||
] %}
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
<br>
|
||||
{{ 'label.phone'|trans }}: {{ model.customer.phone }}
|
||||
{% endif %}
|
||||
{% if model.customer.mail is not empty %}
|
||||
{% if model.customer.email is not empty %}
|
||||
<br>
|
||||
{{ 'label.email'|trans }}: {{ model.customer.mail }}
|
||||
{{ 'label.email'|trans }}: {{ model.customer.email }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
@@ -41,10 +41,10 @@
|
||||
<p contenteditable="true">
|
||||
<b>{{ 'invoice.due_days'|trans }}:</b> {{ model.dueDate|date_short }}
|
||||
{% if model.customer.number is not empty %}
|
||||
<br><b>{{ 'label.customer_number'|trans }}:</b> {{ model.customer.number }}
|
||||
<br><b>{{ 'label.number'|trans }}:</b> {{ model.customer.number }}
|
||||
{% endif %}
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<br><b>{{ 'label.order_number'|trans }}:</b> {{ model.query.project.orderNumber }}
|
||||
<br><b>{{ 'label.orderNumber'|trans }}:</b> {{ model.query.project.orderNumber }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</tr>
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.order_number'|trans }}</th>
|
||||
<th>{{ 'label.orderNumber'|trans }}</th>
|
||||
<td contenteditable="true">
|
||||
{{ model.query.project.orderNumber }}
|
||||
</td>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</tr>
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.order_number'|trans }}</th>
|
||||
<th>{{ 'label.orderNumber'|trans }}</th>
|
||||
<td contenteditable="true">
|
||||
{{ model.query.project.orderNumber }}
|
||||
</td>
|
||||
|
||||
222
templates/macros/actions.html.twig
Normal file
222
templates/macros/actions.html.twig
Normal file
@@ -0,0 +1,222 @@
|
||||
{# This file contains twig macros used to display possible actions for all available entities #}
|
||||
{% macro activities(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {'filter': '#collapseActivityAdmin', 'visibility': '#modal_activity_admin'} %}
|
||||
|
||||
{% if is_granted('create_activity') %}
|
||||
{% set actions = actions|merge({'create': path('admin_activity_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.activities', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro activity(activity, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if activity.id is not empty %}
|
||||
{% if is_granted('edit', activity) %}
|
||||
{% set actions = actions|merge({'edit': path('admin_activity_edit', {'id': activity.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', activity) %}
|
||||
{% set actions = actions|merge({'trash': path('admin_activity_delete', {'id': activity.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% set actions = actions|merge({'list': path('admin_activity')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.activity', {'actions': actions, 'view': view, 'activity': activity}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.button_group(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro projects(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {'filter': '#collapseProjectAdmin', 'visibility': '#modal_project_admin'} %}
|
||||
|
||||
{% if is_granted('create_project') %}
|
||||
{% set actions = actions|merge({'create': path('admin_project_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.projects', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro project(project, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if project.id is not empty %}
|
||||
{% if is_granted('edit', project) %}
|
||||
{% set actions = actions|merge({'edit': path('admin_project_edit', {'id': project.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_activity') %}
|
||||
{% set actions = actions|merge({'activity': path('admin_activity', {'customer': project.customer.id, 'project': project.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', project) %}
|
||||
{% set actions = actions|merge({'trash': path('admin_project_delete', {'id': project.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% set actions = actions|merge({'list': path('admin_project')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.project', {'actions': actions, 'view': view, 'project': project}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.button_group(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro customers(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'filter': '#collapseCustomerAdmin', 'visibility': '#modal_customer_admin'} %}
|
||||
{% if is_granted('create_customer') %}
|
||||
{% set actions = actions|merge({'create': path('admin_customer_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.customers', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro customer(customer, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if customer.id is not empty %}
|
||||
{% if is_granted('edit', customer) %}
|
||||
{% set actions = actions|merge({'edit': path('admin_customer_edit', {'id': customer.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_project') %}
|
||||
{% set actions = actions|merge({'project': path('admin_project', {'customer': customer.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', customer) %}
|
||||
{% set actions = actions|merge({'trash': path('admin_customer_delete', {'id': customer.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% set actions = actions|merge({'list': path('admin_customer')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.customer', {'actions': actions, 'view': view, 'customer': customer}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.button_group(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timesheets(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'filter': '#collapseTimesheet'} %}
|
||||
{% if is_granted('export_own_timesheet') %}
|
||||
{% set actions = actions|merge({'download': {'onclick': 'return exportTimesheet()'}}) %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'visibility': '#modal_timesheet'}) %}
|
||||
{% set actions = actions|merge({'calendar': path('calendar')}) %}
|
||||
{% if is_granted('create_own_timesheet') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('timesheet_create'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.timesheets', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timesheet(timesheet, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if timesheet.id is not empty %}
|
||||
{% if timesheet.end %}
|
||||
{% if is_granted('start', timesheet) %}
|
||||
{% set actions = actions|merge({'repeat': path('timesheet_start', {'id' : timesheet.id})}) %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if is_granted('stop', timesheet) %}
|
||||
{% set actions = actions|merge({'stop': path('timesheet_stop', {'id' : timesheet.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('edit', timesheet) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'edit' %}
|
||||
{% set class = 'modal-ajax-form' %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('timesheet_edit', {'id': timesheet.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('delete', timesheet) %}
|
||||
{% set actions = actions|merge({'trash': path('timesheet_delete', {'id' : timesheet.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% set actions = actions|merge({'list': path('timesheet')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.timesheet', {'actions': actions, 'view': view, 'timesheet': timesheet}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.button_group(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timesheets_team(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'filter': '#collapseTimesheetAdmin'} %}
|
||||
{% if is_granted('export_own_timesheet') %}
|
||||
{% set actions = actions|merge({'download': 'onclick:return exportTimesheet()'}) %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %}
|
||||
{% if is_granted('create_other_timesheet') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('admin_timesheet_create'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.timesheets_team', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timesheet_team(timesheet, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if timesheet.id is not empty %}
|
||||
{% if not timesheet.end and is_granted('stop', timesheet) %}
|
||||
{% set actions = actions|merge({'stop': path('admin_timesheet_stop', {'id' : timesheet.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('edit', timesheet) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'edit' %}
|
||||
{% set class = 'modal-ajax-form' %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('admin_timesheet_edit', {'id': timesheet.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', timesheet) %}
|
||||
{% set actions = actions|merge({'trash': path('admin_timesheet_delete', {'id' : timesheet.id})}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% set actions = actions|merge({'list': path('admin_timesheet')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.timesheet_team', {'actions': actions, 'view': view, 'timesheet': timesheet}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.button_group(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -1,9 +1,11 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_project.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.project(project, 'delete') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_project.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.project(project, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
@@ -2,16 +2,11 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_project.subtitle'|trans }} {{ 'subtitle.amount'|trans({'%count%': entries.count}) }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseProjectAdmin', 'visibility': '#modal_project_admin'} %}
|
||||
{% if is_granted('create_project') %}
|
||||
{% set actions = actions|merge({'create': path('admin_project_create')}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
{% block page_actions %}{{ actions.projects('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseProjectAdmin', showFilter) }}
|
||||
@@ -45,17 +40,7 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">{{ entry.budget|money(entry.customer.currency) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('admin_project_edit', {'id': entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_activity') %}
|
||||
{% set actionButtons = actionButtons|merge({'activity': path('admin_activity', {'customer': entry.customer.id, 'project': entry.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_project_delete', {'id': entry.id})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
{{ actions.project(entry, 'index') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
15
templates/timesheet-team/edit.html.twig
Normal file
15
templates/timesheet-team/edit.html.twig
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.timesheet_team(timesheet, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': (timesheet.id ? 'timesheet.edit'|trans : 'create'|trans),
|
||||
'form': form,
|
||||
'back': path('admin_timesheet')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -2,20 +2,11 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseTimesheetAdmin'} %}
|
||||
{% if is_granted('export_own_timesheet') %}
|
||||
{% set actions = actions|merge({'download': 'onclick:return exportTimesheet()'}) %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %}
|
||||
{% if is_granted('create_other_timesheet') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('admin_timesheet_create'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
{% block page_actions %}{{ actions.timesheets_team('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseTimesheetAdmin', showFilter) }}
|
||||
@@ -74,28 +65,20 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
<a href="{{ path('admin_customer_edit', {'id': entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
||||
{{ widgets.label_customer(entry.project.customer) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||
<a href="{{ path('admin_project_edit', {'id': entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
|
||||
{{ widgets.label_project(entry.project) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">
|
||||
<a href="{{ path('admin_activity_edit', {'id': entry.activity.id}) }}">{{ widgets.label_activity(entry.activity) }}</a>
|
||||
{{ widgets.label_activity(entry.activity) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">
|
||||
{{ widgets.label_user(entry.user) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}"><a href="{{ path('user_profile'|route_alias, {'username' : entry.user.username}) }}">{{ widgets.label_user(entry.user) }}</a></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ entry.description|nl2br }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'edit': {'url': path('admin_timesheet_edit', {'id': entry.id, 'page': page}), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
{% if not entry.end and is_granted('stop', entry) %}
|
||||
{% set actionButtons = {'stop': path('admin_timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_timesheet_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
{{ actions.timesheet_team(entry, 'index') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1,11 +1,14 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.timesheet(timesheet, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': (entry.id ? 'timesheet.edit'|trans : 'create'|trans),
|
||||
'title': (timesheet.id ? 'timesheet.edit'|trans : 'create'|trans),
|
||||
'form': form,
|
||||
'back': path('timesheet')
|
||||
}) }}
|
||||
|
||||
@@ -2,43 +2,11 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
|
||||
{% macro summary(day, duration, dayRates, columns) %}
|
||||
<tr class="summary info">
|
||||
<td class="text-nowrap">{{ day }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-nowrap">{{ duration|duration }}</td>
|
||||
{% if is_granted('view_rate_own_timesheet') %}
|
||||
<td class="text-nowrap">
|
||||
{% for currency, rate in dayRates %}
|
||||
{{ rate|money(currency) }}
|
||||
{% if not loop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td colspan="{{ (columns|length) - 5 }}"></td>
|
||||
{% else %}
|
||||
<td colspan="{{ (columns|length) - 4 }}"></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set actions = {'filter': '#collapseTimesheet'} %}
|
||||
{% if is_granted('export_own_timesheet') %}
|
||||
{% set actions = actions|merge({'download': {'onclick': 'return exportTimesheet()'}}) %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'visibility': '#modal_timesheet'}) %}
|
||||
{% set actions = actions|merge({'calendar': path('calendar')}) %}
|
||||
{% if is_granted('create_own_timesheet') %}
|
||||
{% set actions = actions|merge({'create': {'url': path('timesheet_create'), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
{{ widgets.page_actions(actions) }}
|
||||
{% endblock %}
|
||||
{% block page_actions %}{{ actions.timesheets('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ toolbar.toolbar(toolbarForm, 'collapseTimesheet', showFilter) }}
|
||||
@@ -117,25 +85,7 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">{{ entry.description|desc2html }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'edit': {'url': path('timesheet_edit', {'id': entry.id, 'page': page}), 'class': 'modal-ajax-form'}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if entry.end %}
|
||||
{% if is_granted('start', entry) %}
|
||||
{% set actionButtons = {'repeat': path('timesheet_start', {'id' : entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if is_granted('stop', entry) %}
|
||||
{% set actionButtons = {'stop': path('timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
{{ actions.timesheet(entry, 'index') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if entry.end %}
|
||||
@@ -169,3 +119,25 @@
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% macro summary(day, duration, dayRates, columns) %}
|
||||
<tr class="summary info">
|
||||
<td class="text-nowrap">{{ day }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-nowrap">{{ duration|duration }}</td>
|
||||
{% if is_granted('view_rate_own_timesheet') %}
|
||||
<td class="text-nowrap">
|
||||
{% for currency, rate in dayRates %}
|
||||
{{ rate|money(currency) }}
|
||||
{% if not loop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td colspan="{{ (columns|length) - 5 }}"></td>
|
||||
{% else %}
|
||||
<td colspan="{{ (columns|length) - 4 }}"></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<li><a href="#">{{ 'label.username'|trans }} <span class="pull-right badge bg-blue">{{ user.username }}</span></a></li>
|
||||
<li><a href="#">{{ 'profile.first_entry'|trans }} <span class="pull-right badge bg-purple">{{ stats.firstEntry|date_short }}</span></a></li>
|
||||
{% if is_granted('view_rate_own_timesheet') %}
|
||||
<li><a href="#">{{ 'label.hourly_rate'|trans }} <span class="pull-right badge bg-blue">{{ user.preferenceValue('hourly_rate') }}</span></a></li>
|
||||
<li><a href="#">{{ 'label.hourlyRate'|trans }} <span class="pull-right badge bg-blue">{{ user.preferenceValue('hourly_rate') }}</span></a></li>
|
||||
{% endif %}
|
||||
<li><a href="#">{{ 'label.now'|trans }} <span class="pull-right badge bg-blue">{{ local_time|date_short }} {{ local_time|time }}</span></a></li>
|
||||
</ul>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Tests\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\VisibilityQuery;
|
||||
@@ -31,9 +32,11 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$project = $em->getRepository(Project::class)->find(1);
|
||||
$customer = $em->getRepository(Customer::class)->find(1);
|
||||
|
||||
$project2 = new Project();
|
||||
$project2->setName('Activity Test');
|
||||
$project2->setCustomer($customer);
|
||||
$em->persist($project2);
|
||||
|
||||
$activity = (new Activity())->setName('first one')->setComment('1')->setProject($project2);
|
||||
|
||||
@@ -69,7 +69,7 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
if ($full) {
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'number', 'comment', 'visible', 'company', 'contact', 'address', 'country', 'currency',
|
||||
'phone', 'fax', 'mobile', 'mail', 'timezone'
|
||||
'phone', 'fax', 'mobile', 'email', 'timezone'
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller\Admin;
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\Admin\AboutController
|
||||
* @coversDefaultClass \App\Controller\AboutController
|
||||
* @group integration
|
||||
*/
|
||||
class AboutControllerTest extends ControllerBaseTest
|
||||
@@ -9,8 +9,13 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\ActivityFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Gedmo\Loggable\Entity\LogEntry;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\ActivityController
|
||||
@@ -20,29 +25,229 @@ class ActivityControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/activities/recent');
|
||||
$this->assertUrlIsSecured('/admin/activity/');
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/admin/activity/');
|
||||
}
|
||||
|
||||
public function testRecentActivitiesAction()
|
||||
public function testIndexAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/');
|
||||
$this->assertHasDataTable($client);
|
||||
}
|
||||
|
||||
public function testCreateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/create');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertNull($form->get('activity_edit_form[create_more]')->getValue());
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => [
|
||||
'name' => 'An AcTiVitY Name',
|
||||
'project' => '1',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
|
||||
$this->request($client, '/admin/activity/2/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertEquals('An AcTiVitY Name', $editForm->get('activity_edit_form[name]')->getValue());
|
||||
// make sure customer and project are pre-selected for none global activities
|
||||
$this->assertEquals('1', $editForm->get('activity_edit_form[project]')->getValue());
|
||||
$this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue());
|
||||
}
|
||||
|
||||
public function testCreateActionWithCreateMore()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($user);
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||
$fixture = new ProjectFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/activities/recent');
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/create');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
|
||||
/** @var \Symfony\Component\DomCrawler\Field\ChoiceFormField $project */
|
||||
$project = $form->get('activity_edit_form[project]');
|
||||
$options = $project->availableOptionValues();
|
||||
$selectedProject = $options[array_rand($options)];
|
||||
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => [
|
||||
'name' => 'Test create more',
|
||||
'create_more' => true,
|
||||
'project' => $selectedProject,
|
||||
]
|
||||
]);
|
||||
$this->assertFalse($client->getResponse()->isRedirect());
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertEquals(1, $form->get('activity_edit_form[create_more]')->getValue());
|
||||
$this->assertEquals($selectedProject, $form->get('activity_edit_form[project]')->getValue());
|
||||
}
|
||||
|
||||
public function testEditAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/1/edit');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertFalse($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertEquals('Test', $form->get('activity_edit_form[name]')->getValue());
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => ['name' => 'Test 2']
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertEquals('Test 2', $editForm->get('activity_edit_form[name]')->getValue());
|
||||
// make sure no customer or project is pre-selected for global activities
|
||||
$this->assertEquals('', $editForm->get('activity_edit_form[customer]')->getValue());
|
||||
$this->assertEquals('', $editForm->get('activity_edit_form[project]')->getValue());
|
||||
}
|
||||
|
||||
public function testDeleteAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->request($client, '/admin/activity/1/delete');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasFlashDeleteSuccess($client);
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
|
||||
$this->assertContains('<li class="dropdown notifications-menu">', $content);
|
||||
$this->assertContains('<span class="label label-success">1</span>', $content);
|
||||
$this->assertContains('<a href="/en/timesheet/start/1">', $content);
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
public function testDeleteActionWithTimesheetEntries()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
|
||||
$fixture->setAmount(10);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
$this->assertEquals(10, count($timesheets));
|
||||
|
||||
/** @var Timesheet $entry */
|
||||
foreach ($timesheets as $entry) {
|
||||
$this->assertEquals(1, $entry->getActivity()->getId());
|
||||
}
|
||||
|
||||
$this->request($client, '/admin/activity/1/delete');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=form]')->form();
|
||||
$this->assertStringEndsWith($this->createUrl('/admin/activity/1/delete'), $form->getUri());
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasFlashDeleteSuccess($client);
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
|
||||
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
||||
// $em->clear();
|
||||
// $timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
// $this->assertEquals(0, count($timesheets));
|
||||
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
public function testDeleteActionWithTimesheetEntriesAndReplacement()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
|
||||
$fixture->setAmount(10);
|
||||
$this->importFixture($em, $fixture);
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(1)->setIsGlobal(true)->setIsVisible(true);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
$this->assertEquals(10, count($timesheets));
|
||||
|
||||
/** @var Timesheet $entry */
|
||||
foreach ($timesheets as $entry) {
|
||||
$this->assertEquals(1, $entry->getActivity()->getId());
|
||||
}
|
||||
|
||||
$this->request($client, '/admin/activity/1/delete');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=form]')->form();
|
||||
$this->assertStringEndsWith($this->createUrl('/admin/activity/1/delete'), $form->getUri());
|
||||
$client->submit($form, [
|
||||
'form' => [
|
||||
'activity' => 2
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertHasFlashSuccess($client);
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
$this->assertEquals(10, count($timesheets));
|
||||
|
||||
/** @var Timesheet $entry */
|
||||
foreach ($timesheets as $entry) {
|
||||
$this->assertEquals(2, $entry->getActivity()->getId());
|
||||
}
|
||||
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidationTestData
|
||||
*/
|
||||
public function testValidationForCreateAction(array $formData, array $validationFields)
|
||||
{
|
||||
$this->assertFormHasValidationError(
|
||||
User::ROLE_ADMIN,
|
||||
'/admin/activity/create',
|
||||
'form[name=activity_edit_form]',
|
||||
$formData,
|
||||
$validationFields
|
||||
);
|
||||
}
|
||||
|
||||
public function getValidationTestData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
[
|
||||
'activity_edit_form' => [
|
||||
'name' => '',
|
||||
'project' => 0,
|
||||
]
|
||||
],
|
||||
[
|
||||
'#activity_edit_form_name',
|
||||
'#activity_edit_form_project',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller\Admin;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
use App\Tests\DataFixtures\ActivityFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\Admin\ActivityController
|
||||
* @group integration
|
||||
*/
|
||||
class ActivityControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/admin/activity/');
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/admin/activity/');
|
||||
}
|
||||
|
||||
public function testIndexAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/');
|
||||
$this->assertHasDataTable($client);
|
||||
}
|
||||
|
||||
public function testCreateAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/create');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertNull($form->get('activity_edit_form[create_more]')->getValue());
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => [
|
||||
'name' => 'An AcTiVitY Name',
|
||||
'project' => '1',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
|
||||
$this->request($client, '/admin/activity/2/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertEquals('An AcTiVitY Name', $editForm->get('activity_edit_form[name]')->getValue());
|
||||
// make sure customer and project are pre-selected for none global activities
|
||||
$this->assertEquals('1', $editForm->get('activity_edit_form[project]')->getValue());
|
||||
$this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue());
|
||||
}
|
||||
|
||||
public function testCreateActionWithCreateMore()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new ProjectFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/create');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
|
||||
/** @var \Symfony\Component\DomCrawler\Field\ChoiceFormField $project */
|
||||
$project = $form->get('activity_edit_form[project]');
|
||||
$options = $project->availableOptionValues();
|
||||
$selectedProject = $options[array_rand($options)];
|
||||
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => [
|
||||
'name' => 'Test create more',
|
||||
'create_more' => true,
|
||||
'project' => $selectedProject,
|
||||
]
|
||||
]);
|
||||
$this->assertFalse($client->getResponse()->isRedirect());
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertTrue($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertEquals(1, $form->get('activity_edit_form[create_more]')->getValue());
|
||||
$this->assertEquals($selectedProject, $form->get('activity_edit_form[project]')->getValue());
|
||||
}
|
||||
|
||||
public function testEditAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/1/edit');
|
||||
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertFalse($form->has('activity_edit_form[create_more]'));
|
||||
$this->assertEquals('Test', $form->get('activity_edit_form[name]')->getValue());
|
||||
$client->submit($form, [
|
||||
'activity_edit_form' => ['name' => 'Test 2']
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
|
||||
$this->assertEquals('Test 2', $editForm->get('activity_edit_form[name]')->getValue());
|
||||
// make sure no customer or project is pre-selected for global activities
|
||||
$this->assertEquals('', $editForm->get('activity_edit_form[customer]')->getValue());
|
||||
$this->assertEquals('', $editForm->get('activity_edit_form[project]')->getValue());
|
||||
}
|
||||
|
||||
public function testDeleteAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->request($client, '/admin/activity/1/delete');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasFlashDeleteSuccess($client);
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
public function testDeleteActionWithTimesheetEntries()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
|
||||
$fixture->setAmount(10);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
$this->assertEquals(10, count($timesheets));
|
||||
|
||||
/** @var Timesheet $entry */
|
||||
foreach ($timesheets as $entry) {
|
||||
$this->assertEquals(1, $entry->getActivity()->getId());
|
||||
}
|
||||
|
||||
$this->request($client, '/admin/activity/1/delete');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=form]')->form();
|
||||
$this->assertStringEndsWith($this->createUrl('/admin/activity/1/delete'), $form->getUri());
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasFlashDeleteSuccess($client);
|
||||
$this->assertHasNoEntriesWithFilter($client);
|
||||
|
||||
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
||||
// $em->clear();
|
||||
// $timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
// $this->assertEquals(0, count($timesheets));
|
||||
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
public function testDeleteActionWithTimesheetEntriesAndReplacement()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
|
||||
$fixture->setAmount(10);
|
||||
$this->importFixture($em, $fixture);
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(1)->setIsGlobal(true)->setIsVisible(true);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
$this->assertEquals(10, count($timesheets));
|
||||
|
||||
/** @var Timesheet $entry */
|
||||
foreach ($timesheets as $entry) {
|
||||
$this->assertEquals(1, $entry->getActivity()->getId());
|
||||
}
|
||||
|
||||
$this->request($client, '/admin/activity/1/delete');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=form]')->form();
|
||||
$this->assertStringEndsWith($this->createUrl('/admin/activity/1/delete'), $form->getUri());
|
||||
$client->submit($form, [
|
||||
'form' => [
|
||||
'activity' => 2
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertHasFlashSuccess($client);
|
||||
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
$this->assertEquals(10, count($timesheets));
|
||||
|
||||
/** @var Timesheet $entry */
|
||||
foreach ($timesheets as $entry) {
|
||||
$this->assertEquals(2, $entry->getActivity()->getId());
|
||||
}
|
||||
|
||||
$this->request($client, '/admin/activity/1/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidationTestData
|
||||
*/
|
||||
public function testValidationForCreateAction(array $formData, array $validationFields)
|
||||
{
|
||||
$this->assertFormHasValidationError(
|
||||
User::ROLE_ADMIN,
|
||||
'/admin/activity/create',
|
||||
'form[name=activity_edit_form]',
|
||||
$formData,
|
||||
$validationFields
|
||||
);
|
||||
}
|
||||
|
||||
public function getValidationTestData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
[
|
||||
'activity_edit_form' => [
|
||||
'name' => '',
|
||||
'project' => 0,
|
||||
]
|
||||
],
|
||||
[
|
||||
'#activity_edit_form_name',
|
||||
'#activity_edit_form_project',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,20 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
$client->getResponse()->isSuccessful(),
|
||||
sprintf('The secure URL %s is not protected for role %s', $url, $role)
|
||||
);
|
||||
$this->assertContains('Symfony\Component\Security\Core\Exception\AccessDeniedException', $client->getResponse()->getContent());
|
||||
$this->assertAccessDenied($client);
|
||||
}
|
||||
|
||||
protected function assertAccessDenied(Client $client)
|
||||
{
|
||||
$this->assertFalse(
|
||||
$client->getResponse()->isSuccessful(),
|
||||
'Access is not denied for URL: ' . $client->getRequest()->getUri()
|
||||
);
|
||||
$this->assertContains(
|
||||
'Symfony\Component\Security\Core\Exception\AccessDeniedException',
|
||||
$client->getResponse()->getContent(),
|
||||
'Could not find AccessDeniedException in response'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,10 +244,22 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
*/
|
||||
protected function assertHasNoEntriesWithFilter(Client $client)
|
||||
{
|
||||
$this->assertCalloutWidgetWithMessage($client, 'No entries were found based on your selected filters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $message
|
||||
*/
|
||||
protected function assertCalloutWidgetWithMessage(Client $client, string $message)
|
||||
{
|
||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
||||
$this->assertContains('No entries were found based on your selected filters.', $node->text());
|
||||
$this->assertContains($message, $node->text());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller\Admin;
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Gedmo\Loggable\Entity\LogEntry;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\Admin\CustomerController
|
||||
* @coversDefaultClass \App\Controller\CustomerController
|
||||
* @group integration
|
||||
*/
|
||||
class CustomerControllerTest extends ControllerBaseTest
|
||||
48
tests/Controller/NavbarControllerTest.php
Normal file
48
tests/Controller/NavbarControllerTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\NavbarController
|
||||
* @group integration
|
||||
*/
|
||||
class NavbarControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/activities/recent');
|
||||
}
|
||||
|
||||
public function testRecentActivitiesAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($user);
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/activities/recent');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
|
||||
$this->assertContains('<li class="dropdown notifications-menu">', $content);
|
||||
$this->assertContains('<span class="label label-success">1</span>', $content);
|
||||
$this->assertContains('<a href="/en/timesheet/start/1">', $content);
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,18 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller\Admin;
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Gedmo\Loggable\Entity\LogEntry;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\Admin\ProjectController
|
||||
* @coversDefaultClass \App\Controller\ProjectController
|
||||
* @group integration
|
||||
*/
|
||||
class ProjectControllerTest extends ControllerBaseTest
|
||||
@@ -14,12 +14,13 @@ use App\Entity\User;
|
||||
use App\Form\Type\DateRangeType;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use Gedmo\Loggable\Entity\LogEntry;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\Admin\TimesheetController
|
||||
* @coversDefaultClass \App\Controller\TimesheetTeamController
|
||||
* @group integration
|
||||
*/
|
||||
class TimesheetControllerTest extends ControllerBaseTest
|
||||
class TimesheetTeamControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
@@ -7,13 +7,12 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller\Admin;
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\Admin\UserController
|
||||
* @coversDefaultClass \App\Controller\UserController
|
||||
* @group integration
|
||||
*/
|
||||
class UserControllerTest extends ControllerBaseTest
|
||||
@@ -14,6 +14,7 @@ use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
@@ -271,7 +272,8 @@ class TimesheetFixtures extends Fixture
|
||||
$end = $end->modify('+ ' . (rand(1, 172800)) . ' seconds');
|
||||
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$rate = $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
$hourlyRate = (float) $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
||||
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
@@ -279,7 +281,7 @@ class TimesheetFixtures extends Fixture
|
||||
->setProject($project)
|
||||
->setDescription($description)
|
||||
->setUser($user)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
->setRate($rate)
|
||||
->setBegin($start);
|
||||
|
||||
if ($this->fixedRate) {
|
||||
@@ -287,13 +289,14 @@ class TimesheetFixtures extends Fixture
|
||||
}
|
||||
|
||||
if ($this->hourlyRate) {
|
||||
$entry->setHourlyRate($rate);
|
||||
$entry->setHourlyRate($hourlyRate);
|
||||
}
|
||||
|
||||
if ($setEndDate) {
|
||||
$entry
|
||||
->setEnd($end)
|
||||
->setDuration($duration);
|
||||
->setDuration($duration)
|
||||
;
|
||||
}
|
||||
|
||||
return $entry;
|
||||
|
||||
@@ -36,7 +36,7 @@ class CustomerTest extends AbstractEntityTest
|
||||
$this->assertNull($sut->getPhone());
|
||||
$this->assertNull($sut->getFax());
|
||||
$this->assertNull($sut->getMobile());
|
||||
$this->assertNull($sut->getMail());
|
||||
$this->assertNull($sut->getEmail());
|
||||
$this->assertNull($sut->getHomepage());
|
||||
$this->assertNull($sut->getTimezone());
|
||||
|
||||
@@ -76,8 +76,8 @@ class CustomerTest extends AbstractEntityTest
|
||||
$this->assertInstanceOf(Customer::class, $sut->setMobile('76576534'));
|
||||
$this->assertEquals('76576534', $sut->getMobile());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setMail('test@example.com'));
|
||||
$this->assertEquals('test@example.com', $sut->getMail());
|
||||
$this->assertInstanceOf(Customer::class, $sut->setEmail('test@example.com'));
|
||||
$this->assertEquals('test@example.com', $sut->getEmail());
|
||||
|
||||
$this->assertInstanceOf(Customer::class, $sut->setHomepage('https://www.example.com'));
|
||||
$this->assertEquals('https://www.example.com', $sut->getHomepage());
|
||||
|
||||
@@ -23,8 +23,6 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class RateCalculatorTest extends TestCase
|
||||
{
|
||||
public const HOURLY_RATE = 75;
|
||||
|
||||
public function testCalculateWithTimesheetHourlyRate()
|
||||
{
|
||||
$record = new Timesheet();
|
||||
@@ -130,7 +128,7 @@ class RateCalculatorTest extends TestCase
|
||||
$this->assertEquals($exptectedRate, $timesheet->getRate());
|
||||
}
|
||||
|
||||
protected function getTestUser($rate = self::HOURLY_RATE)
|
||||
protected function getTestUser($rate = 75)
|
||||
{
|
||||
$pref = new UserPreference();
|
||||
$pref->setName(UserPreference::HOURLY_RATE);
|
||||
@@ -159,23 +157,23 @@ class RateCalculatorTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the hourly rate from user_preferences to calculate the rate.
|
||||
*
|
||||
* @dataProvider getRuleDefinitions
|
||||
*/
|
||||
public function testCalculateWithRules($rules, $expectedFactor)
|
||||
public function testCalculateWithRulesByUsersHourlyRate($duration, $rules, $expectedRate)
|
||||
{
|
||||
$seconds = 31837;
|
||||
|
||||
$end = new \DateTime();
|
||||
$end->setTimezone(new \DateTimeZone('UTC'));
|
||||
$end->setTime(12, 0, 0);
|
||||
$start = clone $end;
|
||||
$start->setTimezone(new \DateTimeZone('UTC'));
|
||||
$start->setTimestamp($end->getTimestamp() - $seconds);
|
||||
$start->setTimestamp($end->getTimestamp() - $duration);
|
||||
|
||||
$record = new Timesheet();
|
||||
$record->setUser($this->getTestUser());
|
||||
$record->setBegin($start);
|
||||
$record->setDuration($seconds);
|
||||
$record->setDuration($duration);
|
||||
$record->setActivity(new Activity());
|
||||
|
||||
$this->assertEquals(0, $record->getRate());
|
||||
@@ -185,10 +183,7 @@ class RateCalculatorTest extends TestCase
|
||||
$sut = new RateCalculator($rules);
|
||||
$sut->calculate($record);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->rateForSeconds(self::HOURLY_RATE, $seconds) * $expectedFactor,
|
||||
$record->getRate()
|
||||
);
|
||||
$this->assertEquals($expectedRate, $record->getRate());
|
||||
}
|
||||
|
||||
public function getRuleDefinitions()
|
||||
@@ -200,10 +195,12 @@ class RateCalculatorTest extends TestCase
|
||||
|
||||
return [
|
||||
[
|
||||
31837,
|
||||
[],
|
||||
1
|
||||
663.27
|
||||
],
|
||||
[
|
||||
31837,
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
@@ -214,9 +211,10 @@ class RateCalculatorTest extends TestCase
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
2.0
|
||||
1326.54
|
||||
],
|
||||
[
|
||||
31837,
|
||||
[
|
||||
'default' => [
|
||||
'days' => [$day],
|
||||
@@ -227,13 +225,8 @@ class RateCalculatorTest extends TestCase
|
||||
'factor' => 1.5
|
||||
],
|
||||
],
|
||||
3.5
|
||||
2321.45
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function rateForSeconds($hourlyRate, $seconds)
|
||||
{
|
||||
return (float) $hourlyRate * ($seconds / 3600);
|
||||
}
|
||||
}
|
||||
|
||||
43
tests/Timesheet/UtilTest.php
Normal file
43
tests/Timesheet/UtilTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Timesheet;
|
||||
|
||||
use App\Timesheet\Util;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Timesheet\Util
|
||||
*/
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getRateCalculationData
|
||||
*/
|
||||
public function testCalculateRate($hourlyRate, $duration, $expectedRate)
|
||||
{
|
||||
$this->assertEquals($expectedRate, Util::calculateRate($hourlyRate, $duration));
|
||||
}
|
||||
|
||||
public function getRateCalculationData()
|
||||
{
|
||||
yield [0, 0, 0];
|
||||
yield [1, 100, 0.03];
|
||||
yield [1, 900, 0.25];
|
||||
yield [1, 1800, 0.5];
|
||||
yield [10000, 1, 2.78];
|
||||
yield [736, 123.45, 25.15];
|
||||
yield [736, 123, 25.15];
|
||||
yield [7360, 1234.99, 2522.84];
|
||||
yield [7360, 1234, 2522.84];
|
||||
yield [7360.34, 1234, 2522.96];
|
||||
yield [7360.01, 1234, 2522.85];
|
||||
yield [7360.99, 1234, 2523.18];
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class DateExtensionsTest extends TestCase
|
||||
|
||||
public function testGetFilters()
|
||||
{
|
||||
$filters = ['month_name', 'date_short', 'date_time', 'date_format', 'time', 'hour24'];
|
||||
$filters = ['month_name', 'date_short', 'date_time', 'date_full', 'date_format', 'time', 'hour24'];
|
||||
$sut = $this->getSut('de', []);
|
||||
$twigFilters = $sut->getFilters();
|
||||
$this->assertCount(count($filters), $twigFilters);
|
||||
@@ -148,4 +148,17 @@ class DateExtensionsTest extends TestCase
|
||||
]);
|
||||
$this->assertEquals('foo', $sut->hour24('foo', 'bar'));
|
||||
}
|
||||
|
||||
public function testDateTimeFull()
|
||||
{
|
||||
$sut = $this->getSut('en', [
|
||||
'en' => ['date_time_type' => 'yyyy-MM-dd HH:mm:ss'],
|
||||
]);
|
||||
|
||||
$dateTime = new \DateTime('2019-08-17 12:29:47', new \DateTimeZone(date_default_timezone_get()));
|
||||
$dateTime->setDate(2019, 8, 17);
|
||||
$dateTime->setTime(12, 29, 47);
|
||||
|
||||
$this->assertEquals('2019-08-17 12:29:47', $sut->dateTimeFull($dateTime));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Tests\Twig;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Twig\Extensions;
|
||||
use App\Utils\LocaleSettings;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -60,7 +61,7 @@ class ExtensionsTest extends TestCase
|
||||
|
||||
public function testGetFunctions()
|
||||
{
|
||||
$functions = ['locales', 'is_visible_column', 'is_datatable_configured'];
|
||||
$functions = ['locales', 'is_visible_column', 'is_datatable_configured', 'class_name'];
|
||||
$sut = $this->getSut($this->localeDe);
|
||||
$twigFunctions = $sut->getFunctions();
|
||||
$this->assertCount(count($functions), $twigFunctions);
|
||||
@@ -244,4 +245,14 @@ class ExtensionsTest extends TestCase
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetClassName()
|
||||
{
|
||||
$sut = $this->getSut($this->localeEn);
|
||||
$this->assertEquals('DateTime', $sut->getClassName(new \DateTime()));
|
||||
$this->assertEquals('stdClass', $sut->getClassName(new \stdClass()));
|
||||
$this->assertNull($sut->getClassName(''));
|
||||
$this->assertNull($sut->getClassName(null));
|
||||
$this->assertEquals('App\Entity\User', $sut->getClassName(new User()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +183,8 @@
|
||||
<source>label.project</source>
|
||||
<target>مشروع</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.hourly_rate">
|
||||
<source>label.hourly_rate</source>
|
||||
<trans-unit id="label.hourlyRate">
|
||||
<source>label.hourlyRate</source>
|
||||
<target>بالساعة</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.skin">
|
||||
@@ -311,8 +311,8 @@
|
||||
<source>modal.columns.description</source>
|
||||
<target>عند حفظ الخانات غير المحددة سيتم إخفاؤها وسيتم تخزين هذا الإعداد في ملف تعريف ارتباط المتصفح. إذا قمت بحذف ملفات تعريف الارتباط الخاصة بك ، سيتم عكس الإعدادات.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.fixed_rate">
|
||||
<source>label.fixed_rate</source>
|
||||
<trans-unit id="label.fixedRate">
|
||||
<source>label.fixedRate</source>
|
||||
<target>معدل ثابت</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -439,8 +439,8 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>عميلك هو الأساس لإنشاء المشاريع والأنشطة</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<trans-unit id="label.number">
|
||||
<source>label.number</source>
|
||||
<target>حساب</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
@@ -741,8 +741,8 @@
|
||||
<source>invoice.total_working_time</source>
|
||||
<target>المدة الإجمالية</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<trans-unit id="label.orderNumber">
|
||||
<source>label.orderNumber</source>
|
||||
<target>رقم الطلب</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
|
||||
@@ -187,8 +187,8 @@
|
||||
<source>label.project</source>
|
||||
<target>Projekt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.hourly_rate">
|
||||
<source>label.hourly_rate</source>
|
||||
<trans-unit id="label.hourlyRate">
|
||||
<source>label.hourlyRate</source>
|
||||
<target>Stundenlohn</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.skin">
|
||||
@@ -319,8 +319,8 @@
|
||||
<source>modal.columns.description</source>
|
||||
<target>Beim Speichern werden die nicht ausgewählten Spalten ausgeblendet und diese Einstellungen in einem Browser Cookie hinterlegt. Sollten Sie Ihre Cookies löschen, werden die Einstellungen rückgängig gemacht.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.fixed_rate">
|
||||
<source>label.fixed_rate</source>
|
||||
<trans-unit id="label.fixedRate">
|
||||
<source>label.fixedRate</source>
|
||||
<target>Festbetrag</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -490,8 +490,8 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Ihre Kunden stellen die Basis dar, auf der Sie Projekte und Tätigkeiten anlegen können</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<trans-unit id="label.number">
|
||||
<source>label.number</source>
|
||||
<target>Kundennummer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
@@ -810,8 +810,8 @@
|
||||
<source>invoice.total_working_time</source>
|
||||
<target>Gesamtdauer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<trans-unit id="label.orderNumber">
|
||||
<source>label.orderNumber</source>
|
||||
<target>Bestellnummer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
|
||||
@@ -187,8 +187,8 @@
|
||||
<source>label.project</source>
|
||||
<target>Project</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.hourly_rate">
|
||||
<source>label.hourly_rate</source>
|
||||
<trans-unit id="label.hourlyRate">
|
||||
<source>label.hourlyRate</source>
|
||||
<target>Hourly rate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.skin">
|
||||
@@ -319,8 +319,8 @@
|
||||
<source>modal.columns.description</source>
|
||||
<target>Upon saving the un-checked columns will be hidden and this setting will be stored in a browser cookie. If you delete your cookies, the settings will be reversed.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.fixed_rate">
|
||||
<source>label.fixed_rate</source>
|
||||
<trans-unit id="label.fixedRate">
|
||||
<source>label.fixedRate</source>
|
||||
<target>Fixed rate</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -490,8 +490,8 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Your customer are the base for creating projects and activities</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<trans-unit id="label.number">
|
||||
<source>label.number</source>
|
||||
<target>Account</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
@@ -810,8 +810,8 @@
|
||||
<source>invoice.total_working_time</source>
|
||||
<target>Total duration</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<trans-unit id="label.orderNumber">
|
||||
<source>label.orderNumber</source>
|
||||
<target>Order number</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
|
||||
@@ -191,8 +191,8 @@
|
||||
<source>label.project</source>
|
||||
<target>Proyecto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.hourly_rate">
|
||||
<source>label.hourly_rate</source>
|
||||
<trans-unit id="label.hourlyRate">
|
||||
<source>label.hourlyRate</source>
|
||||
<target>Tarifa por hora</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.skin">
|
||||
@@ -443,8 +443,8 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Sus clientes son la base para crear proyectos y actividades</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<trans-unit id="label.number">
|
||||
<source>label.number</source>
|
||||
<target>Cuenta</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
@@ -701,8 +701,8 @@
|
||||
<source>invoice.signature_customer</source>
|
||||
<target>Confirmación: Fecha / Nombre / Firma</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<trans-unit id="label.orderNumber">
|
||||
<source>label.orderNumber</source>
|
||||
<target>Número de orden</target>
|
||||
</trans-unit>
|
||||
|
||||
|
||||
@@ -191,8 +191,8 @@
|
||||
<source>label.project</source>
|
||||
<target>Projet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.hourly_rate">
|
||||
<source>label.hourly_rate</source>
|
||||
<trans-unit id="label.hourlyRate">
|
||||
<source>label.hourlyRate</source>
|
||||
<target>Tarif horaire</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.skin">
|
||||
@@ -443,8 +443,8 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Les clients sont à la base de la création des projets et des activités.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<trans-unit id="label.number">
|
||||
<source>label.number</source>
|
||||
<target>Compte</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
@@ -734,8 +734,8 @@
|
||||
<source>invoice.signature_customer</source>
|
||||
<target>Pour certification : date / nom du client / signature</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<trans-unit id="label.orderNumber">
|
||||
<source>label.orderNumber</source>
|
||||
<target>Numéro d'ordre</target>
|
||||
</trans-unit>
|
||||
|
||||
|
||||
@@ -184,8 +184,8 @@
|
||||
<source>label.project</source>
|
||||
<target>Projekt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.hourly_rate">
|
||||
<source>label.hourly_rate</source>
|
||||
<trans-unit id="label.hourlyRate">
|
||||
<source>label.hourlyRate</source>
|
||||
<target>Órabér</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.skin">
|
||||
@@ -312,8 +312,8 @@
|
||||
<source>modal.columns.description</source>
|
||||
<target>A megjelenítendő oszlopok egy böngésző sütiben lesznek tárolva. Ha törlöd az oldalhoz tartozó sütiket, akkor ez a beállítás visszaáll az alapértelmezett értékre.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.fixed_rate">
|
||||
<source>label.fixed_rate</source>
|
||||
<trans-unit id="label.fixedRate">
|
||||
<source>label.fixedRate</source>
|
||||
<target>Fix órabér</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -457,8 +457,8 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Az ügyfelekhez hozzárendelve hozhatsz létre projekteket és tevékenységeket</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<trans-unit id="label.number">
|
||||
<source>label.number</source>
|
||||
<target>Adószám</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
@@ -761,8 +761,8 @@
|
||||
<source>invoice.total_working_time</source>
|
||||
<target>Teljes munkaidő</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.order_number">
|
||||
<source>label.order_number</source>
|
||||
<trans-unit id="label.orderNumber">
|
||||
<source>label.orderNumber</source>
|
||||
<target>Megbízás száma</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_tax_number">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user