detail pages for customers and projects (#1371)
This commit is contained in:
24
UPGRADING.md
24
UPGRADING.md
@@ -12,18 +12,28 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||
|
||||
New permissions:
|
||||
|
||||
- `comments_customer` - show comment list on customer detail page (new feature)
|
||||
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
|
||||
- `comments_project` - show comment list on project detail page (new feature)
|
||||
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
|
||||
|
||||
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
|
||||
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
|
||||
|
||||
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
|
||||
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
|
||||
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
|
||||
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
|
||||
|
||||
### ExpenseBundle
|
||||
|
||||
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
|
||||
|
||||
If you use the ExpensePlugin, you should delete it before updating: `rm -r var/plugins/ExpenseBundle`.
|
||||
You will run into an error otherwise during the update.
|
||||
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
|
||||
|
||||
After the Kimai update was successful, you have to re-install the latest version, which is compatible with Kimai 1.7 only.
|
||||
|
||||
### Hosting
|
||||
|
||||
- New feature requires the timezone date within MySQL/MariaDB. This is especially important for self-hosted systems: [you need to import the data manually](https://mariadb.com/kb/en/library/mysql_tzinfo_to_sql/).
|
||||
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
|
||||
|
||||
### Developer
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ require('./sass/app.scss');
|
||||
// ------ Kimai itself ------
|
||||
require('./js/KimaiWebLoader.js');
|
||||
global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default;
|
||||
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
|
||||
|
||||
// ------ Autocomplete for tags only ------
|
||||
require('jquery-ui/ui/widgets/autocomplete');
|
||||
72
assets/js/widgets/KimaiReloadPageWidget.js
Normal file
72
assets/js/widgets/KimaiReloadPageWidget.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiReloadPageWidget: a simple helper to reload the page on events
|
||||
*/
|
||||
|
||||
import jQuery from "jquery";
|
||||
|
||||
export default class KimaiReloadPageWidget {
|
||||
|
||||
constructor(events, fullReload) {
|
||||
this.overlay = jQuery('<div class="overlay-wrapper"><div class="overlay"><div class="fa fa-refresh fa-spin"></div></div></div>');
|
||||
this.widget = jQuery('div.content-wrapper');
|
||||
|
||||
const self = this;
|
||||
|
||||
const reloadPage = function (event) {
|
||||
if (fullReload) {
|
||||
document.location.reload(true);
|
||||
} else {
|
||||
self.loadPage(document.location);
|
||||
}
|
||||
};
|
||||
|
||||
for (const eventName of events.split(' ')) {
|
||||
document.addEventListener(eventName, reloadPage);
|
||||
}
|
||||
}
|
||||
|
||||
static create(events, fullReload) {
|
||||
if (fullReload === undefined || fullReload === null) {
|
||||
fullReload = false;
|
||||
}
|
||||
return new KimaiReloadPageWidget(events, fullReload);
|
||||
}
|
||||
|
||||
_showOverlay() {
|
||||
this.widget.append(this.overlay);
|
||||
}
|
||||
|
||||
_hideOverlay() {
|
||||
jQuery(this.overlay).remove();
|
||||
}
|
||||
|
||||
loadPage(url) {
|
||||
const self = this;
|
||||
|
||||
self._showOverlay();
|
||||
|
||||
jQuery.ajax({
|
||||
url: url,
|
||||
data: {},
|
||||
success: function (response) {
|
||||
jQuery('section.content').replaceWith(
|
||||
jQuery(response).find('section.content')
|
||||
);
|
||||
self._hideOverlay();
|
||||
},
|
||||
dataType: 'html',
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
self._hideOverlay();
|
||||
document.location = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,4 +48,9 @@ td {
|
||||
|
||||
.label-gray {
|
||||
background-color: $gray-lte;
|
||||
}
|
||||
|
||||
/* Delete link in action dropdowns */
|
||||
.dropdown-menu > li.delete > a {
|
||||
color: #dd4b39;
|
||||
}
|
||||
@@ -23,10 +23,7 @@ span.label-customer {
|
||||
table.dataTable {
|
||||
/* action column */
|
||||
.actions {
|
||||
width: 90px;
|
||||
li.delete a {
|
||||
color: #dd4b39;
|
||||
}
|
||||
width: 40px;
|
||||
}
|
||||
tr {
|
||||
/* summary row - provided in the users own timesheet table */
|
||||
|
||||
@@ -279,5 +279,8 @@ table.dataTable.table > tbody > tr > td {
|
||||
.img-circle {
|
||||
max-width: 35px;
|
||||
margin-right: 10px;
|
||||
&.teamlead {
|
||||
box-shadow: 0 0 5px 3px $gray-lte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
bin/console
19
bin/console
@@ -1,23 +1,24 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
if (!file_exists(__DIR__.'/../vendor/autoload.php')) {
|
||||
echo 'Warning: You need to run "composer install" before you can use the console.' . \PHP_EOL;
|
||||
die(1);
|
||||
}
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Debug\Debug;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
if (!class_exists(Application::class)) {
|
||||
throw new \RuntimeException('You need to add "symfony/framework-bundle" as a Composer dependency.');
|
||||
}
|
||||
|
||||
if (!isset($_SERVER['APP_ENV'])) {
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
|
||||
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables or change the variables in your .env file.');
|
||||
}
|
||||
(new Dotenv(true))->load(__DIR__.'/../.env');
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"phpstan/phpstan": "^0.11.7",
|
||||
"phpstan/phpstan-doctrine": "^0.11.4",
|
||||
"phpstan/phpstan-phpunit": "^0.11.2",
|
||||
"phpstan/phpstan-symfony": "^0.11.6",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpstan/phpstan-doctrine": "^0.12",
|
||||
"phpstan/phpstan-phpunit": "^0.12",
|
||||
"phpstan/phpstan-symfony": "^0.12",
|
||||
"phpunit/phpunit": "^8.0",
|
||||
"symfony/browser-kit": "^4.0",
|
||||
"symfony/css-selector": "^4.0",
|
||||
|
||||
917
composer.lock
generated
917
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -86,17 +86,21 @@ kimai:
|
||||
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
|
||||
sets:
|
||||
ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity']
|
||||
ACTIVITIES_TEAM: ['view_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity']
|
||||
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project']
|
||||
PROJECTS_TEAM: ['view_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project']
|
||||
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer']
|
||||
CUSTOMERS_TEAM: ['view_customer','edit_teamlead_customer','budget_teamlead_customer']
|
||||
ACTIVITIES_TEAMLEAD: ['view_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity']
|
||||
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project','comments_project','details_project']
|
||||
PROJECTS_ALL_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project','comments_teamlead_project','details_teamlead_project']
|
||||
PROJECTS_ALL_TEAM: ['view_team_project','edit_team_project','budget_team_project','comments_team_project','details_team_project']
|
||||
PROJECTS_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','comments_teamlead_project','details_teamlead_project']
|
||||
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer','comments_customer','details_customer']
|
||||
CUSTOMERS_ALL_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','permissions_teamlead_customer','comments_teamlead_customer','details_teamlead_customer']
|
||||
CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','comments_team_customer','details_team_customer']
|
||||
CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','details_teamlead_customer']
|
||||
INVOICE: ['view_invoice','create_invoice']
|
||||
INVOICE_TEMPLATE: ['manage_invoice_template']
|
||||
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
|
||||
TAGS: ['view_tag','manage_tag','delete_tag']
|
||||
USER: ['view_user','create_user','delete_user','role_permissions']
|
||||
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
||||
@@ -107,10 +111,10 @@ kimai:
|
||||
SINGLE_USER: ['view_team_member','budget_team_project']
|
||||
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member']
|
||||
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member']
|
||||
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member']
|
||||
# link above sets to one complete set for each user role
|
||||
ROLE_USER: ['@TIMESHEET','@PROFILE','@SINGLE_USER']
|
||||
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAM','@PROJECTS_TEAM','@CUSTOMERS_TEAM','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@SINGLE_TEAMLEAD']
|
||||
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAMLEAD','@PROJECTS_TEAMLEAD','@CUSTOMERS_TEAMLEAD','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@SINGLE_TEAMLEAD']
|
||||
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_ADMIN']
|
||||
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_SUPER_ADMIN']
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
@@ -119,6 +123,8 @@ kimai:
|
||||
ROLE_TEAMLEAD: ['ROLE_TEAMLEAD']
|
||||
ROLE_ADMIN: ['ROLE_ADMIN']
|
||||
ROLE_SUPER_ADMIN: ['ROLE_SUPER_ADMIN']
|
||||
# only here to register the (partially) unused permissions in the UI
|
||||
ROLE_FAKE: ['CUSTOMERS_ALL_TEAMLEAD','CUSTOMERS_ALL_TEAM','PROJECTS_ALL_TEAMLEAD','PROJECTS_ALL_TEAM']
|
||||
# add or remove single permissions
|
||||
roles:
|
||||
ROLE_USER: []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
- vendor/phpstan/phpstan-symfony/rules.neon
|
||||
|
||||
parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
@@ -10,15 +11,15 @@ parameters:
|
||||
# container_xml_path: '%rootDir%/../../../var/cache/dev/srcApp_KernelDevDebugContainer.xml'
|
||||
ignoreErrors:
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::scalarNode\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::children\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::useAttributeAsKey\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::integerNode\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::booleanNode\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::end\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::defaultValue\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::addDefaultsIfNotSet\(\).#'
|
||||
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::requiresAtLeastOneElement\(\).#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$bs.#'
|
||||
- '#Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required.#'
|
||||
- '#Method App\\Controller\\AbstractController::getUser\(\) should return App\\Entity\\User|null but returns object|null. #'
|
||||
excludes_analyse:
|
||||
- %rootDir%/../../../src/Command/KimaiImporterCommand.php
|
||||
- %rootDir%/../../../src/Ldap/LdapDriver.php
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
public/build/app.6ea4e95f.js
Normal file
1
public/build/app.6ea4e95f.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,10 +5,10 @@
|
||||
"build/runtime.6ca1eea5.js",
|
||||
"build/0.1a6ffb43.js",
|
||||
"build/1.16767f92.js",
|
||||
"build/app.055e71d1.js"
|
||||
"build/app.6ea4e95f.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.483d7949.css"
|
||||
"build/app.8240996d.css"
|
||||
]
|
||||
},
|
||||
"chart": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"build/runtime.6ca1eea5.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.1a6ffb43.js": "sha384-vuVCyLVj2b4h3jpKl+JSANRCacmXeLk+ihK5T5NZBO7+SJ5Y+o6d4qju116hTPSD",
|
||||
"build/1.16767f92.js": "sha384-JPoKdrVtBemSiVBoAnmSxLML7xXM9zYeuwOPYQv/kLzt/P4cmLY5r9gH8oaGRPFG",
|
||||
"build/app.055e71d1.js": "sha384-NGXBt+8vQ5h4orQ5B82W9GoeVpu4bNmiJI7k/goKm1ogp2JGvo0tyadJdoOf3Dt+",
|
||||
"build/app.483d7949.css": "sha384-u8/TZyq87MTQTKScxUGmbJcWEBhsoXAlsUTB8XSgKvuufZ5bODWYm1o1E1DRosJu",
|
||||
"build/app.6ea4e95f.js": "sha384-mHnCng8TBnNQoqH7ZSnkexDEYgODkjaDJIN5BpK6dSAP7dhIcQ5dUY19NtgfjUsZ",
|
||||
"build/app.8240996d.css": "sha384-ZaS4ro6HGbDfLyaJc8cVXJzTd8plX3mRzhHhiiUEErSzPlRKdZfViDk23BNvAaz+",
|
||||
"build/2.dad56560.js": "sha384-oKsefSmRq0GL+Qa6f6jExXvFay3cM6YZFHQRKoAHi2p54UOCtjJfjF2eAB1qykoL",
|
||||
"build/chart.7f4d7f29.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
"build/calendar.9e7cdb6a.js": "sha384-RvFQJC1YvKq9BqRDObc5REPbi9ZiG5/fkqKP1xLJUlFDb2b8WqTR/f4F+hdE9cI1",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"build/0.1a6ffb43.js": "build/0.1a6ffb43.js",
|
||||
"build/1.16767f92.js": "build/1.16767f92.js",
|
||||
"build/2.dad56560.js": "build/2.dad56560.js",
|
||||
"build/app.css": "build/app.483d7949.css",
|
||||
"build/app.js": "build/app.055e71d1.js",
|
||||
"build/app.css": "build/app.8240996d.css",
|
||||
"build/app.js": "build/app.6ea4e95f.js",
|
||||
"build/calendar.css": "build/calendar.ade7bcdf.css",
|
||||
"build/calendar.js": "build/calendar.9e7cdb6a.js",
|
||||
"build/chart.js": "build/chart.7f4d7f29.js",
|
||||
|
||||
@@ -23,6 +23,10 @@ class ThemeConfiguration implements SystemBundleConfiguration, \ArrayAccess
|
||||
return (bool) $this->find('auto_reload_datatable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently unused, as JS selects are always activated.
|
||||
* @deprecated since 1.7 will be removed with 2.0
|
||||
*/
|
||||
public function getSelectPicker(): string
|
||||
{
|
||||
return (string) $this->find('select_type');
|
||||
|
||||
@@ -11,17 +11,24 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Team;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Form\CustomerCommentForm;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\CustomerTeamPermissionForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -34,42 +41,28 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage customer in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/customer")
|
||||
* @Security("is_granted('view_customer')")
|
||||
* @Security("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')")
|
||||
*/
|
||||
class CustomerController extends AbstractController
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(CustomerRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(CustomerRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\CustomerRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_customer')")
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -85,7 +78,7 @@ class CustomerController extends AbstractController
|
||||
$query->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('customer/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
@@ -99,7 +92,7 @@ class CustomerController extends AbstractController
|
||||
* @param CustomerQuery $query
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
protected function findMetaColumns(CustomerQuery $query): array
|
||||
private function findMetaColumns(CustomerQuery $query): array
|
||||
{
|
||||
$event = new CustomerMetaDisplayEvent($query, CustomerMetaDisplayEvent::CUSTOMER);
|
||||
$this->dispatcher->dispatch($event);
|
||||
@@ -111,16 +104,16 @@ class CustomerController extends AbstractController
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
public function createAction(Request $request, FormConfiguration $configuration)
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->configuration->getCustomerDefaultTimezone()) {
|
||||
$timezone = $this->configuration->getCustomerDefaultTimezone();
|
||||
if (null !== $configuration->getCustomerDefaultTimezone()) {
|
||||
$timezone = $configuration->getCustomerDefaultTimezone();
|
||||
}
|
||||
|
||||
$customer = new Customer();
|
||||
$customer->setCountry($this->configuration->getCustomerDefaultCountry());
|
||||
$customer->setCurrency($this->configuration->getCustomerDefaultCurrency());
|
||||
$customer->setCountry($configuration->getCustomerDefaultCountry());
|
||||
$customer->setCurrency($configuration->getCustomerDefaultCurrency());
|
||||
$customer->setTimezone($timezone);
|
||||
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
@@ -130,7 +123,7 @@ class CustomerController extends AbstractController
|
||||
* @Route(path="/{id}/permissions", name="admin_customer_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', customer)")
|
||||
*/
|
||||
public function teamPermissions(Customer $customer, Request $request)
|
||||
public function teamPermissionsAction(Customer $customer, Request $request)
|
||||
{
|
||||
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
||||
@@ -141,11 +134,11 @@ class CustomerController extends AbstractController
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveCustomer($customer);
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
@@ -157,18 +150,160 @@ class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_customer_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', customer)")
|
||||
* @Route(path="/{id}/comment_delete", name="customer_comment_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")
|
||||
*/
|
||||
public function budgetAction(Customer $customer)
|
||||
public function deleteCommentAction(CustomerComment $comment)
|
||||
{
|
||||
$stats = $this->getRepository()->getCustomerStatistics($customer);
|
||||
$customerId = $comment->getCustomer()->getId();
|
||||
|
||||
// TODO sent event with stats
|
||||
try {
|
||||
$this->repository->deleteComment($comment);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->render('customer/budget.html.twig', [
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customerId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_add", name="customer_comment_add", methods={"POST"})
|
||||
* @Security("is_granted('edit', customer) and is_granted('comments', customer)")
|
||||
*/
|
||||
public function addCommentAction(Customer $customer, Request $request)
|
||||
{
|
||||
$comment = new CustomerComment();
|
||||
$form = $this->getCommentForm($customer, $comment);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->repository->saveComment($comment);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_pin", name="customer_comment_pin", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getCustomer()) and is_granted('comments', comment.getCustomer())")
|
||||
*/
|
||||
public function pinCommentAction(CustomerComment $comment)
|
||||
{
|
||||
$comment->setPinned(!$comment->isPinned());
|
||||
try {
|
||||
$this->repository->saveComment($comment);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $comment->getCustomer()->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/create_team", name="customer_team_create", methods={"GET"})
|
||||
* @Security("is_granted('create_team') and is_granted('permissions', customer)")
|
||||
*/
|
||||
public function createDefaultTeamAction(Customer $customer, TeamRepository $teamRepository)
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Team already existing']);
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
$defaultTeam = new Team();
|
||||
$defaultTeam->setName($customer->getName());
|
||||
$defaultTeam->setTeamLead($this->getUser());
|
||||
$defaultTeam->addCustomer($customer);
|
||||
|
||||
try {
|
||||
$teamRepository->saveTeam($defaultTeam);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/projects/{page}", defaults={"page": 1}, name="customer_projects", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', customer)")
|
||||
*/
|
||||
public function projectsAction(Customer $customer, int $page, ProjectRepository $projectRepository)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
$query->setPageSize(5);
|
||||
$query->setCustomer($customer);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $projectRepository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('customer/embed_projects.html.twig', [
|
||||
'customer' => $customer,
|
||||
'projects' => $entries,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="customer_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', customer)")
|
||||
*/
|
||||
public function detailsAction(Customer $customer, TeamRepository $teamRepository)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$stats = null;
|
||||
$timezone = null;
|
||||
$defaultTeam = null;
|
||||
$commentForm = null;
|
||||
$attachments = [];
|
||||
$comments = null;
|
||||
$teams = null;
|
||||
$projects = null;
|
||||
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
$commentForm = $this->getCommentForm($customer, new CustomerComment())->createView();
|
||||
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $customer->getTimezone()) {
|
||||
$timezone = new \DateTimeZone($customer->getTimezone());
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget', $customer)) {
|
||||
$stats = $this->repository->getCustomerStatistics($customer);
|
||||
}
|
||||
|
||||
if ($this->isGranted('comments', $customer)) {
|
||||
$comments = $this->repository->getComments($customer);
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $customer) || $this->isGranted('details', $customer) || $this->isGranted('view_team')) {
|
||||
$teams = $customer->getTeams();
|
||||
}
|
||||
|
||||
return $this->render('customer/details.html.twig', [
|
||||
'customer' => $customer,
|
||||
'comments' => $comments,
|
||||
'commentForm' => $commentForm,
|
||||
'attachments' => $attachments,
|
||||
'stats' => $stats,
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'now' => new \DateTime('now', $timezone),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -187,7 +322,7 @@ class CustomerController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(Customer $customer, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getCustomerStatistics($customer);
|
||||
$stats = $this->repository->getCustomerStatistics($customer);
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
@@ -215,7 +350,7 @@ class CustomerController extends AbstractController
|
||||
|
||||
if ($deleteForm->isSubmitted() && $deleteForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->deleteCustomer($customer, $deleteForm->get('customer')->getData());
|
||||
$this->repository->deleteCustomer($customer, $deleteForm->get('customer')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
@@ -236,21 +371,18 @@ class CustomerController extends AbstractController
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
private function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveCustomer($customer);
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
@@ -262,7 +394,7 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
private function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(CustomerToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_customer', [
|
||||
@@ -272,8 +404,24 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function getCommentForm(Customer $customer, CustomerComment $comment): FormInterface
|
||||
{
|
||||
if (null === $comment->getId()) {
|
||||
$comment->setCustomer($customer);
|
||||
$comment->setCreatedBy($this->getUser());
|
||||
}
|
||||
|
||||
return $this->createForm(CustomerCommentForm::class, $comment, [
|
||||
'action' => $this->generateUrl('customer_comment_add', ['id' => $customer->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createEditForm(Customer $customer): FormInterface
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
if ($customer->getId() === null) {
|
||||
$url = $this->generateUrl('admin_customer_create');
|
||||
} else {
|
||||
|
||||
@@ -13,16 +13,21 @@ use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Form\ProjectCommentForm;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\ProjectTeamPermissionForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use App\Repository\TeamRepository;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -33,12 +38,12 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage projects in the admin part of the site.
|
||||
* Controller used to manage projects.
|
||||
*
|
||||
* @Route(path="/admin/project")
|
||||
* @Security("is_granted('view_project')")
|
||||
* @Security("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')")
|
||||
*/
|
||||
class ProjectController extends AbstractController
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
@@ -51,7 +56,7 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
@@ -60,15 +65,9 @@ class ProjectController extends AbstractController
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getRepository(): ProjectRepository
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_project')")
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -85,7 +84,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
$entries = $this->repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('project/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
@@ -122,11 +121,11 @@ class ProjectController extends AbstractController
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveProject($project);
|
||||
$this->repository->saveProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_project');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
@@ -154,18 +153,154 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_project_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', project)")
|
||||
* @Route(path="/{id}/comment_delete", name="project_comment_delete", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")
|
||||
*/
|
||||
public function budgetAction(Project $project)
|
||||
public function deleteCommentAction(ProjectComment $comment)
|
||||
{
|
||||
$stats = $this->getRepository()->getProjectStatistics($project);
|
||||
$projectId = $comment->getProject()->getId();
|
||||
|
||||
// TODO sent event with stats
|
||||
try {
|
||||
$this->repository->deleteComment($comment);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->render('project/budget.html.twig', [
|
||||
return $this->redirectToRoute('project_details', ['id' => $projectId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_add", name="project_comment_add", methods={"POST"})
|
||||
* @Security("is_granted('edit', project) and is_granted('comments', project)")
|
||||
*/
|
||||
public function addCommentAction(Project $project, Request $request)
|
||||
{
|
||||
$comment = new ProjectComment();
|
||||
$form = $this->getCommentForm($project, $comment);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->repository->saveComment($comment);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/comment_pin", name="project_comment_pin", methods={"GET"})
|
||||
* @Security("is_granted('edit', comment.getProject()) and is_granted('comments', comment.getProject())")
|
||||
*/
|
||||
public function pinCommentAction(ProjectComment $comment)
|
||||
{
|
||||
$comment->setPinned(!$comment->isPinned());
|
||||
try {
|
||||
$this->repository->saveComment($comment);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $comment->getProject()->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/create_team", name="project_team_create", methods={"GET"})
|
||||
* @Security("is_granted('create_team') and is_granted('edit', project)")
|
||||
*/
|
||||
public function createDefaultTeamAction(Project $project, TeamRepository $teamRepository)
|
||||
{
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
if (null !== $defaultTeam) {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Team already existing']);
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
$defaultTeam = new Team();
|
||||
$defaultTeam->setName($project->getName());
|
||||
$defaultTeam->setTeamLead($this->getUser());
|
||||
$defaultTeam->addProject($project);
|
||||
|
||||
try {
|
||||
$teamRepository->saveTeam($defaultTeam);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/activities/{page}", defaults={"page": 1}, name="project_activities", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', project)")
|
||||
*/
|
||||
public function activitiesAction(Project $project, int $page, ActivityRepository $activityRepository)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
$query->setPageSize(5);
|
||||
$query->setProject($project);
|
||||
$query->setExcludeGlobals(true);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $activityRepository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('project/embed_activities.html.twig', [
|
||||
'project' => $project,
|
||||
'stats' => $stats
|
||||
'activities' => $entries,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/details", name="project_details", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', project)")
|
||||
*/
|
||||
public function detailsAction(Project $project, TeamRepository $teamRepository)
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$stats = null;
|
||||
$defaultTeam = null;
|
||||
$commentForm = null;
|
||||
$attachments = [];
|
||||
$comments = null;
|
||||
$teams = null;
|
||||
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
$commentForm = $this->getCommentForm($project, new ProjectComment())->createView();
|
||||
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isGranted('budget', $project)) {
|
||||
$stats = $this->repository->getProjectStatistics($project);
|
||||
}
|
||||
|
||||
if ($this->isGranted('comments', $project)) {
|
||||
$comments = $this->repository->getComments($project);
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $project) || $this->isGranted('details', $project) || $this->isGranted('view_team')) {
|
||||
$teams = $project->getTeams();
|
||||
}
|
||||
|
||||
return $this->render('project/details.html.twig', [
|
||||
'project' => $project,
|
||||
'comments' => $comments,
|
||||
'commentForm' => $commentForm,
|
||||
'attachments' => $attachments,
|
||||
'stats' => $stats,
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -184,7 +319,7 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(Project $project, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getProjectStatistics($project);
|
||||
$stats = $this->repository->getProjectStatistics($project);
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
@@ -213,9 +348,9 @@ class ProjectController extends AbstractController
|
||||
|
||||
if ($deleteForm->isSubmitted() && $deleteForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->deleteProject($project, $deleteForm->get('project')->getData());
|
||||
$this->repository->deleteProject($project, $deleteForm->get('project')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -234,17 +369,14 @@ class ProjectController extends AbstractController
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
protected function renderProjectForm(Project $project, Request $request)
|
||||
private function renderProjectForm(Project $project, Request $request)
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveProject($project);
|
||||
$this->repository->saveProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
@@ -254,9 +386,9 @@ class ProjectController extends AbstractController
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$project = $newProject;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_project');
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
@@ -277,8 +409,24 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function getCommentForm(Project $project, ProjectComment $comment): FormInterface
|
||||
{
|
||||
if (null === $comment->getId()) {
|
||||
$comment->setProject($project);
|
||||
$comment->setCreatedBy($this->getUser());
|
||||
}
|
||||
|
||||
return $this->createForm(ProjectCommentForm::class, $comment, [
|
||||
'action' => $this->generateUrl('project_comment_add', ['id' => $project->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createEditForm(Project $project): FormInterface
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||
$url = $this->generateUrl('admin_project_create');
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Event\SystemConfigurationEvent;
|
||||
use App\Form\Model\Configuration;
|
||||
use App\Form\Model\SystemConfiguration as SystemConfigurationModel;
|
||||
use App\Form\SystemConfigurationForm;
|
||||
use App\Form\Type\EnhancedSelectboxType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\RoundingModeType;
|
||||
use App\Form\Type\SkinType;
|
||||
@@ -293,25 +292,20 @@ class SystemConfigurationController extends AbstractController
|
||||
->setSection(SystemConfigurationModel::SECTION_THEME)
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('theme.select_type')
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setType(EnhancedSelectboxType::class)
|
||||
->setRequired(false),
|
||||
->setName('theme.autocomplete_chars')
|
||||
->setLabel('theme.autocomplete_chars')
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('timesheet.markdown_content')
|
||||
->setLabel('theme.markdown_content')
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
(new Configuration())
|
||||
->setName('theme.autocomplete_chars')
|
||||
->setLabel('theme.autocomplete_chars')
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
// FIXME should that be configurable per user?
|
||||
// TODO should that be configurable per user?
|
||||
/*
|
||||
(new Configuration())
|
||||
->setName('theme.auto_reload_datatable')
|
||||
->setLabel('theme.auto_reload_datatable') // FIXME translation
|
||||
->setLabel('theme.auto_reload_datatable') // TODO translation
|
||||
->setType(CheckboxType::class)
|
||||
->setTranslationDomain('system-configuration'),
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,6 @@ use App\Form\TeamProjectForm;
|
||||
use App\Form\Toolbar\TeamToolbarForm;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
@@ -28,7 +27,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* @Route(path="/admin/teams")
|
||||
* @Security("is_granted('view_team')")
|
||||
*/
|
||||
class TeamController extends AbstractController
|
||||
final class TeamController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
@@ -93,6 +92,40 @@ class TeamController extends AbstractController
|
||||
return $this->renderEditScreen($team, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit_member", name="admin_team_member", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', team)")
|
||||
*/
|
||||
public function editMemberAction(Team $team, Request $request)
|
||||
{
|
||||
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||
'action' => $this->generateUrl('admin_team_member', ['id' => $team->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
// make sure that the teamlead is always part of the team, otherwise permission checks
|
||||
// and filtering might not work as expected!
|
||||
$team->addUser($team->getTeamLead());
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('team/edit_member.html.twig', [
|
||||
'team' => $team,
|
||||
'form' => $editForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderEditScreen(Team $team, Request $request): Response
|
||||
{
|
||||
$customerForm = null;
|
||||
@@ -122,7 +155,7 @@ class TeamController extends AbstractController
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
@@ -142,7 +175,7 @@ class TeamController extends AbstractController
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +194,7 @@ class TeamController extends AbstractController
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class TeamFixtures extends Fixture implements DependentFixtureInterface
|
||||
public const BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @return class-string[]
|
||||
*/
|
||||
public function getDependencies()
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
public const BATCH_SIZE = 100;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @return class-string[]
|
||||
*/
|
||||
public function getDependencies()
|
||||
{
|
||||
|
||||
@@ -31,10 +31,10 @@ class Configuration implements ConfigurationInterface
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder('kimai');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
$rootNode = $treeBuilder->getRootNode();
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $treeBuilder->getRootNode();
|
||||
|
||||
$rootNode
|
||||
$node
|
||||
->children()
|
||||
->scalarNode('data_dir')
|
||||
->isRequired()
|
||||
@@ -75,7 +75,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getTimesheetNode()
|
||||
{
|
||||
$builder = new TreeBuilder('timesheet');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -205,7 +205,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getInvoiceNode()
|
||||
{
|
||||
$builder = new TreeBuilder('invoice');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -232,7 +232,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getLanguagesNode()
|
||||
{
|
||||
$builder = new TreeBuilder('languages');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -256,7 +256,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getCalendarNode()
|
||||
{
|
||||
$builder = new TreeBuilder('calendar');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -310,7 +310,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getThemeNode()
|
||||
{
|
||||
$builder = new TreeBuilder('theme');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -326,6 +326,7 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->scalarNode('select_type')
|
||||
->defaultValue('selectpicker')
|
||||
->setDeprecated()
|
||||
->end()
|
||||
->scalarNode('auto_reload_datatable')
|
||||
->defaultFalse()
|
||||
@@ -374,7 +375,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getIndustryNode()
|
||||
{
|
||||
$builder = new TreeBuilder('industry');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -390,7 +391,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getUserNode()
|
||||
{
|
||||
$builder = new TreeBuilder('user');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -411,7 +412,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getWidgetsNode()
|
||||
{
|
||||
$builder = new TreeBuilder('widgets');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -438,7 +439,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getDashboardNode()
|
||||
{
|
||||
$builder = new TreeBuilder('dashboard');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -467,7 +468,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getDefaultsNode()
|
||||
{
|
||||
$builder = new TreeBuilder('defaults');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
@@ -499,7 +500,7 @@ class Configuration implements ConfigurationInterface
|
||||
protected function getPermissionsNode()
|
||||
{
|
||||
$builder = new TreeBuilder('permissions');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
$node = $builder->getRootNode();
|
||||
|
||||
$node
|
||||
|
||||
31
src/Entity/CommentInterface.php
Normal file
31
src/Entity/CommentInterface.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\Entity;
|
||||
|
||||
interface CommentInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getMessage(): ?string;
|
||||
|
||||
public function setMessage(string $message);
|
||||
|
||||
public function getCreatedBy(): ?User;
|
||||
|
||||
public function setCreatedBy(User $createdBy);
|
||||
|
||||
public function getCreatedAt(): ?\DateTime;
|
||||
|
||||
public function setCreatedAt(\DateTime $createdAt);
|
||||
|
||||
public function isPinned(): bool;
|
||||
|
||||
public function setPinned(bool $pinned);
|
||||
}
|
||||
104
src/Entity/CommentTableTypeTrait.php
Normal file
104
src/Entity/CommentTableTypeTrait.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait CommentTableTypeTrait
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="message", type="text", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $message;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $createdBy;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="created_at", type="datetime", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $createdAt;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="pinned", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $pinned = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(string $message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getCreatedBy(): ?User
|
||||
{
|
||||
return $this->createdBy;
|
||||
}
|
||||
|
||||
public function setCreatedBy(User $createdBy)
|
||||
{
|
||||
$this->createdBy = $createdBy;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTime $createdAt)
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
}
|
||||
|
||||
public function isPinned(): bool
|
||||
{
|
||||
return $this->pinned;
|
||||
}
|
||||
|
||||
public function setPinned(bool $pinned)
|
||||
{
|
||||
$this->pinned = $pinned;
|
||||
}
|
||||
}
|
||||
49
src/Entity/CustomerComment.php
Normal file
49
src/Entity/CustomerComment.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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 App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_customers_comments",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"customer_id"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class CustomerComment implements CommentInterface
|
||||
{
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
public function setCustomer(Customer $customer): CustomerComment
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
}
|
||||
49
src/Entity/ProjectComment.php
Normal file
49
src/Entity/ProjectComment.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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 App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_projects_comments",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"project_id"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ProjectComment implements CommentInterface
|
||||
{
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
public function setProject(Project $project): ProjectComment
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
}
|
||||
@@ -93,13 +93,13 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu->addChild($timesheets);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_customer')) {
|
||||
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
|
||||
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $this->getIcon('customer'));
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'admin_customer_budget', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$menu->addChild($customers);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project')) {
|
||||
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
|
||||
$projects = new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], $this->getIcon('project'));
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'admin_project_budget', 'admin_project_edit', 'admin_project_delete']);
|
||||
$menu->addChild($projects);
|
||||
|
||||
@@ -59,7 +59,7 @@ class ActivityEditForm extends AbstractType
|
||||
],
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
'label' => 'label.comment',
|
||||
'label' => 'label.description',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
|
||||
47
src/Form/CustomerCommentForm.php
Normal file
47
src/Form/CustomerCommentForm.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\CustomerComment;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerCommentForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('message', TextareaType::class, [
|
||||
'label' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerComment::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_customer_comment',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerComment'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class CustomerEditForm extends AbstractType
|
||||
'required' => false,
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
'label' => 'label.comment',
|
||||
'label' => 'label.description',
|
||||
'required' => false,
|
||||
])
|
||||
->add('company', TextType::class, [
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use App\Configuration\ThemeConfiguration;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
@@ -20,19 +19,12 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
/**
|
||||
* Converts normal select boxes into javascript enhanced versions.
|
||||
*/
|
||||
class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
{
|
||||
public const TYPE_SELECTPICKER = 'selectpicker';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* @deprecated since 1.7 will be removed with 2.0
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
public function __construct(ThemeConfiguration $configuration)
|
||||
{
|
||||
$this->type = $configuration->getSelectPicker();
|
||||
}
|
||||
public const TYPE_SELECTPICKER = 'selectpicker';
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
@@ -46,10 +38,6 @@ class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
if ($this->type !== self::TYPE_SELECTPICKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($options['selectpicker']) && false === $options['selectpicker']) {
|
||||
return;
|
||||
}
|
||||
|
||||
47
src/Form/ProjectCommentForm.php
Normal file
47
src/Form/ProjectCommentForm.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Form;
|
||||
|
||||
use App\Entity\ProjectComment;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectCommentForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('message', TextareaType::class, [
|
||||
'label' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectComment::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_project_comment',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectComment'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class ProjectEditForm extends AbstractType
|
||||
$entry = $options['data'];
|
||||
$id = $entry->getId();
|
||||
|
||||
if ($id !== null) {
|
||||
if (null !== $entry->getCustomer()) {
|
||||
$customer = $entry->getCustomer();
|
||||
$options['currency'] = $customer->getCurrency();
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class ProjectEditForm extends AbstractType
|
||||
],
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
'label' => 'label.comment',
|
||||
'label' => 'label.description',
|
||||
'required' => false,
|
||||
])
|
||||
->add('orderNumber', TextType::class, [
|
||||
|
||||
65
src/Migrations/Version20200109102138.php
Normal file
65
src/Migrations/Version20200109102138.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Creates comment tables for customers and projects
|
||||
*
|
||||
* @version 1.7
|
||||
*/
|
||||
final class Version20200109102138 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Creates comment tables for customers and projects';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$customerComment = $schema->createTable('kimai2_customers_comments');
|
||||
$customerComment->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$customerComment->addColumn('customer_id', 'integer', ['notnull' => true]);
|
||||
$customerComment->addColumn('message', 'text', ['notnull' => true]);
|
||||
$customerComment->addColumn('created_by_id', 'integer', ['notnull' => true]);
|
||||
$customerComment->addColumn('created_at', 'datetime', ['notnull' => true]);
|
||||
$customerComment->addColumn('pinned', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$customerComment->setPrimaryKey(['id']);
|
||||
$customerComment->addIndex(['customer_id'], 'IDX_A5B142D99395C3F3');
|
||||
$customerComment->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A5B142D99395C3F3');
|
||||
$customerComment->addForeignKeyConstraint('kimai2_users', ['created_by_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A5B142D9B03A8386');
|
||||
|
||||
$projectComment = $schema->createTable('kimai2_projects_comments');
|
||||
$projectComment->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$projectComment->addColumn('project_id', 'integer', ['notnull' => true]);
|
||||
$projectComment->addColumn('message', 'text', ['notnull' => true]);
|
||||
$projectComment->addColumn('created_by_id', 'integer', ['notnull' => true]);
|
||||
$projectComment->addColumn('created_at', 'datetime', ['notnull' => true]);
|
||||
$projectComment->addColumn('pinned', 'boolean', ['notnull' => true, 'default' => false]);
|
||||
$projectComment->setPrimaryKey(['id']);
|
||||
$projectComment->addIndex(['project_id'], 'IDX_29A23638166D1F9C');
|
||||
$projectComment->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_29A23638166D1F9C');
|
||||
$projectComment->addForeignKeyConstraint('kimai2_users', ['created_by_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_29A23638B03A8386');
|
||||
|
||||
$this->addSql('DELETE from kimai2_configuration WHERE name = "theme.select_type"');
|
||||
$this->addSql('DELETE from kimai2_roles_permissions WHERE permission = "delete_other_profile"');
|
||||
$this->addSql('DELETE from kimai2_roles_permissions WHERE permission = "delete_own_profile"');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('kimai2_projects_comments');
|
||||
$schema->dropTable('kimai2_customers_comments');
|
||||
}
|
||||
}
|
||||
@@ -294,12 +294,15 @@ class ActivityRepository extends EntityRepository
|
||||
if ($query->isGlobalsOnly()) {
|
||||
$where->add($qb->expr()->isNull('a.project'));
|
||||
} elseif (null !== $query->getProject()) {
|
||||
$where->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('a.project', ':project'),
|
||||
$qb->expr()->isNull('a.project')
|
||||
)
|
||||
$orX = $qb->expr()->orX(
|
||||
$qb->expr()->eq('a.project', ':project')
|
||||
);
|
||||
|
||||
if (!$query->isExcludeGlobals()) {
|
||||
$orX->add($qb->expr()->isNull('a.project'));
|
||||
}
|
||||
|
||||
$where->add($orX);
|
||||
$qb->setParameter('project', $query->getProject());
|
||||
} elseif (null !== $query->getCustomer()) {
|
||||
$where->add('p.customer = :customer');
|
||||
@@ -343,6 +346,11 @@ class ActivityRepository extends EntityRepository
|
||||
}
|
||||
}
|
||||
|
||||
// this will make sure, that we do not accidentally create results with multiple rows
|
||||
// => which would result in a wrong LIMIT / pagination results
|
||||
// the second group by is needed due to SQL standard (even though logically not really required for this query)
|
||||
$qb->addGroupBy('a.id')->addGroupBy($orderBy);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -352,6 +360,7 @@ class ActivityRepository extends EntityRepository
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->resetDQLPart('groupBy')
|
||||
->select($qb->expr()->countDistinct('a.id'))
|
||||
;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
@@ -208,9 +209,13 @@ class CustomerRepository extends EntityRepository
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('c')
|
||||
$qb
|
||||
->select('c')
|
||||
->from(Customer::class, 'c')
|
||||
->orderBy('c.' . $query->getOrderBy(), $query->getOrder());
|
||||
;
|
||||
|
||||
$orderBy = 'c.' . $query->getOrderBy();
|
||||
$qb->orderBy($orderBy, $query->getOrder());
|
||||
|
||||
if (CustomerQuery::SHOW_VISIBLE == $query->getVisibility()) {
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':visible'));
|
||||
@@ -260,6 +265,11 @@ class CustomerRepository extends EntityRepository
|
||||
}
|
||||
}
|
||||
|
||||
// this will make sure, that we do not accidentally create results with multiple rows
|
||||
// => which would result in a wrong LIMIT / pagination results
|
||||
// the second group by is needed due to SQL standard (even though logically not really required for this query)
|
||||
$qb->addGroupBy('c.id')->addGroupBy($orderBy);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -278,6 +288,7 @@ class CustomerRepository extends EntityRepository
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->resetDQLPart('groupBy')
|
||||
->select($qb->expr()->countDistinct('c.id'))
|
||||
;
|
||||
|
||||
@@ -336,4 +347,33 @@ class CustomerRepository extends EntityRepository
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
public function getComments(Customer $customer): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->select('comments')
|
||||
->from(CustomerComment::class, 'comments')
|
||||
->andWhere($qb->expr()->eq('comments.customer', ':customer'))
|
||||
->addOrderBy('comments.pinned', 'DESC')
|
||||
->addOrderBy('comments.createdAt', 'DESC')
|
||||
->setParameter('customer', $customer)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function saveComment(CustomerComment $comment)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($comment);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteComment(CustomerComment $comment)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($comment);
|
||||
$entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\ProjectStatistic;
|
||||
@@ -351,6 +352,11 @@ class ProjectRepository extends EntityRepository
|
||||
}
|
||||
}
|
||||
|
||||
// this will make sure, that we do not accidentally create results with multiple rows
|
||||
// => which would result in a wrong LIMIT / pagination results
|
||||
// the second group by is needed due to SQL standard (even though logically not really required for this query)
|
||||
$qb->addGroupBy('p.id')->addGroupBy($orderBy);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
@@ -360,6 +366,7 @@ class ProjectRepository extends EntityRepository
|
||||
$qb
|
||||
->resetDQLPart('select')
|
||||
->resetDQLPart('orderBy')
|
||||
->resetDQLPart('groupBy')
|
||||
->select($qb->expr()->countDistinct('p.id'))
|
||||
;
|
||||
|
||||
@@ -438,4 +445,33 @@ class ProjectRepository extends EntityRepository
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
public function getComments(Project $project): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->select('comments')
|
||||
->from(ProjectComment::class, 'comments')
|
||||
->andWhere($qb->expr()->eq('comments.project', ':project'))
|
||||
->addOrderBy('comments.pinned', 'DESC')
|
||||
->addOrderBy('comments.createdAt', 'DESC')
|
||||
->setParameter('project', $project)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function saveComment(ProjectComment $comment)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($comment);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteComment(ProjectComment $comment)
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($comment);
|
||||
$entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ class ActivityQuery extends ProjectQuery
|
||||
* @var bool
|
||||
*/
|
||||
private $globalsOnly = false;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $excludeGlobals = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -54,6 +58,18 @@ class ActivityQuery extends ProjectQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isExcludeGlobals(): bool
|
||||
{
|
||||
return (bool) $this->excludeGlobals;
|
||||
}
|
||||
|
||||
public function setExcludeGlobals(bool $excludeGlobals): self
|
||||
{
|
||||
$this->excludeGlobals = (bool) $excludeGlobals;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project|int|null
|
||||
*/
|
||||
|
||||
@@ -82,6 +82,7 @@ class DateExtensions extends AbstractExtension
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dateShort($date)
|
||||
{
|
||||
@@ -99,6 +100,7 @@ class DateExtensions extends AbstractExtension
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dateTime($date)
|
||||
{
|
||||
@@ -110,14 +112,16 @@ class DateExtensions extends AbstractExtension
|
||||
$date = new DateTime($date);
|
||||
}
|
||||
|
||||
return date_format($date, $this->dateTimeFormat);
|
||||
return $date->format($this->dateTimeFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
* @param bool $userTimezone
|
||||
* @return bool|false|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dateTimeFull($date)
|
||||
public function dateTimeFull($date, bool $userTimezone = true)
|
||||
{
|
||||
if (null === $this->dateTimeTypeFormat) {
|
||||
$this->dateTimeTypeFormat = $this->localeSettings->getDateTimeTypeFormat();
|
||||
@@ -127,11 +131,17 @@ class DateExtensions extends AbstractExtension
|
||||
$date = new DateTime($date);
|
||||
}
|
||||
|
||||
$timezone = date_default_timezone_get();
|
||||
|
||||
if (!$userTimezone) {
|
||||
$timezone = $date->getTimezone()->getName();
|
||||
}
|
||||
|
||||
$formatter = new \IntlDateFormatter(
|
||||
$this->localeSettings->getLocale(),
|
||||
\IntlDateFormatter::MEDIUM,
|
||||
\IntlDateFormatter::MEDIUM,
|
||||
date_default_timezone_get(),
|
||||
$timezone,
|
||||
\IntlDateFormatter::GREGORIAN,
|
||||
$this->dateTimeTypeFormat
|
||||
);
|
||||
@@ -143,6 +153,7 @@ class DateExtensions extends AbstractExtension
|
||||
* @param DateTime|string $date
|
||||
* @param string $format
|
||||
* @return false|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dateFormat($date, string $format)
|
||||
{
|
||||
@@ -156,6 +167,7 @@ class DateExtensions extends AbstractExtension
|
||||
/**
|
||||
* @param DateTime|string $date
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function time($date)
|
||||
{
|
||||
|
||||
@@ -40,12 +40,14 @@ final class IconExtension extends AbstractExtension
|
||||
'edit' => 'far fa-edit',
|
||||
'end' => 'fas fa-stopwatch',
|
||||
'export' => 'fas fa-file-export',
|
||||
'fax' => 'fas fa-fax',
|
||||
'filter' => 'fas fa-filter',
|
||||
'help' => 'far fa-question-circle',
|
||||
'home' => 'fas fa-home',
|
||||
'invoice' => 'fas fa-file-invoice-dollar',
|
||||
'invoice-template' => 'fas fa-file-signature',
|
||||
'list' => 'fas fa-list',
|
||||
'locked' => 'fas fa-lock',
|
||||
'logout' => 'fas fa-sign-out-alt',
|
||||
'mail' => 'fas fa-envelope-open',
|
||||
'mail-sent' => 'fas fa-paper-plane',
|
||||
@@ -55,6 +57,7 @@ final class IconExtension extends AbstractExtension
|
||||
'ods' => 'fas fa-table',
|
||||
'off' => 'fas fa-toggle-off',
|
||||
'on' => 'fas fa-toggle-on',
|
||||
'pin' => 'fas fa-thumbtack',
|
||||
'pdf' => 'fas fa-file-pdf',
|
||||
'pause' => 'fas fa-pause',
|
||||
'pause-small' => 'far fa-pause-circle',
|
||||
|
||||
@@ -15,9 +15,9 @@ use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* A twig extension to handle markdown parser.
|
||||
* A twig extension to handle markdown content.
|
||||
*/
|
||||
class MarkdownExtension extends AbstractExtension
|
||||
final class MarkdownExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var Markdown
|
||||
@@ -26,10 +26,9 @@ class MarkdownExtension extends AbstractExtension
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* MarkdownExtension constructor.
|
||||
* @param Markdown $parser
|
||||
*/
|
||||
public function __construct(Markdown $parser, TimesheetConfiguration $configuration)
|
||||
@@ -46,10 +45,36 @@ class MarkdownExtension extends AbstractExtension
|
||||
return [
|
||||
new TwigFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('desc2html', [$this, 'timesheetContent'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('comment2html', [$this, 'timesheetContent'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('comment2html', [$this, 'commentContent'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the entities comment (customer, project, activity ...) into HTML.
|
||||
*
|
||||
* @param string $content
|
||||
* @param bool $fullLength
|
||||
* @return string
|
||||
*/
|
||||
public function commentContent(?string $content, bool $fullLength = false): string
|
||||
{
|
||||
if (empty($content)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$fullLength && strlen($content) > 101) {
|
||||
$content = trim(substr($content, 0, 100)) . ' …';
|
||||
}
|
||||
|
||||
if ($this->configuration->isMarkdownEnabled()) {
|
||||
$content = $this->markdown->toHtml($content, false);
|
||||
} elseif ($fullLength) {
|
||||
$content = '<p>' . nl2br($content) . '</p>';
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the timesheet description content into HTML.
|
||||
*
|
||||
|
||||
@@ -12,19 +12,18 @@ namespace App\Utils;
|
||||
/**
|
||||
* A simple class to parse markdown syntax and return HTML.
|
||||
*/
|
||||
class Markdown
|
||||
final class Markdown
|
||||
{
|
||||
/**
|
||||
* @var ParsedownExtension
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* Markdown constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->parser = new ParsedownExtension();
|
||||
$this->parser->setUrlsLinked(true);
|
||||
$this->parser->setBreaksEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -93,8 +93,12 @@ class ActivityVoter extends AbstractVoter
|
||||
}
|
||||
}
|
||||
|
||||
if (null === ($customer = $project->getCustomer())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Team $team */
|
||||
foreach ($project->getCustomer()->getTeams() as $team) {
|
||||
foreach ($customer->getTeams() as $team) {
|
||||
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,25 +15,22 @@ use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on Customers.
|
||||
* A voter to check authorization on Customers.
|
||||
*/
|
||||
class CustomerVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const BUDGET = 'budget';
|
||||
public const DELETE = 'delete';
|
||||
public const PERMISSIONS = 'permissions';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Customer)
|
||||
* supported attributes/rules based on the given customer
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::BUDGET,
|
||||
self::DELETE,
|
||||
self::PERMISSIONS,
|
||||
'view',
|
||||
'create',
|
||||
'edit',
|
||||
'budget',
|
||||
'delete',
|
||||
'permissions',
|
||||
'comments',
|
||||
'details',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -72,6 +69,11 @@ class CustomerVoter extends AbstractVoter
|
||||
return true;
|
||||
}
|
||||
|
||||
// those cannot be assigned to teams
|
||||
if (in_array($attribute, ['create', 'delete'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_customer');
|
||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_customer');
|
||||
|
||||
|
||||
@@ -19,21 +19,17 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
*/
|
||||
class ProjectVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const BUDGET = 'budget';
|
||||
public const DELETE = 'delete';
|
||||
public const PERMISSIONS = 'permissions';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Project)
|
||||
* support rules based on the given project
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::BUDGET,
|
||||
self::DELETE,
|
||||
self::PERMISSIONS,
|
||||
'view',
|
||||
'edit',
|
||||
'budget',
|
||||
'delete',
|
||||
'permissions',
|
||||
'comments',
|
||||
'details',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -72,6 +68,11 @@ class ProjectVoter extends AbstractVoter
|
||||
return true;
|
||||
}
|
||||
|
||||
// those cannot be assigned to teams
|
||||
if (in_array($attribute, ['create', 'delete'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_project');
|
||||
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_project');
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget
|
||||
$options = parent::getOptions($options);
|
||||
|
||||
if (empty($options['id'])) {
|
||||
$options['id'] = uniqid('UserTeamProjects_');
|
||||
$options['id'] = 'WidgetUserTeamProjects';
|
||||
}
|
||||
|
||||
return $options;
|
||||
|
||||
@@ -17,7 +17,7 @@ class UserTeams extends SimpleWidget implements AuthorizedWidget
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
$this->setId('UserTeams');
|
||||
$this->setTitle('label.teams');
|
||||
$this->setTitle('label.my_teams');
|
||||
$this->setOptions([
|
||||
'user' => $user->getUser(),
|
||||
'id' => '',
|
||||
@@ -29,7 +29,7 @@ class UserTeams extends SimpleWidget implements AuthorizedWidget
|
||||
$options = parent::getOptions($options);
|
||||
|
||||
if (empty($options['id'])) {
|
||||
$options['id'] = uniqid('UserTeams_');
|
||||
$options['id'] = 'WidgetUserTeams';
|
||||
}
|
||||
|
||||
return $options;
|
||||
|
||||
30
symfony.lock
30
symfony.lock
@@ -201,9 +201,6 @@
|
||||
"jdorn/sql-formatter": {
|
||||
"version": "v1.2.17"
|
||||
},
|
||||
"jean85/pretty-package-versions": {
|
||||
"version": "1.2"
|
||||
},
|
||||
"jms/metadata": {
|
||||
"version": "1.6.0"
|
||||
},
|
||||
@@ -267,30 +264,6 @@
|
||||
"nesbot/carbon": {
|
||||
"version": "2.24.0"
|
||||
},
|
||||
"nette/bootstrap": {
|
||||
"version": "v3.0.0"
|
||||
},
|
||||
"nette/di": {
|
||||
"version": "v3.0.0"
|
||||
},
|
||||
"nette/finder": {
|
||||
"version": "v2.5.0"
|
||||
},
|
||||
"nette/neon": {
|
||||
"version": "v3.0.0"
|
||||
},
|
||||
"nette/php-generator": {
|
||||
"version": "v3.2.2"
|
||||
},
|
||||
"nette/robot-loader": {
|
||||
"version": "v3.2.0"
|
||||
},
|
||||
"nette/schema": {
|
||||
"version": "v1.0.0"
|
||||
},
|
||||
"nette/utils": {
|
||||
"version": "v3.0.1"
|
||||
},
|
||||
"nikic/php-parser": {
|
||||
"version": "v4.0.2"
|
||||
},
|
||||
@@ -342,9 +315,6 @@
|
||||
"phpspec/prophecy": {
|
||||
"version": "1.7.3"
|
||||
},
|
||||
"phpstan/phpdoc-parser": {
|
||||
"version": "0.3.3"
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "0.11.7"
|
||||
},
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
{% if is_granted('budget', activity) %}
|
||||
{% set actions = actions|merge({'report': {'url': path('admin_activity_budget', {'id': activity.id})}}) %}
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_other_timesheet') %}
|
||||
{% set actions = actions|merge({'timesheet': path('admin_timesheet', {'customer': activity.project ? activity.project.customer.id : null, 'project': activity.project ? activity.project.id : null, 'activity': activity.id})}) %}
|
||||
{% endif %}
|
||||
@@ -39,12 +42,12 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% if view != 'index' and view != 'custom' %}
|
||||
{% set actions = actions|merge({'back': path('admin_activity')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.activity', {'actions': actions, 'view': view, 'activity': activity}) %}
|
||||
{% if view == 'index' %}
|
||||
{% if view == 'index' or view == 'custom' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
|
||||
@@ -4,19 +4,23 @@
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "activity/actions.html.twig" as actions %}
|
||||
|
||||
{% set showVisibility = query.visibility != 1 %}
|
||||
{% set columns = {
|
||||
'name': 'alwaysVisible',
|
||||
'customer': 'hidden-xs',
|
||||
'project': 'hidden-xs',
|
||||
'comment': 'hidden-xs hidden-sm',
|
||||
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
|
||||
} %}
|
||||
{% for field in metaColumns %}
|
||||
{% set columns = columns|merge({
|
||||
('mf_' ~ field.name): {'title': field.label, 'class': 'hidden-xs hidden-sm', 'orderBy': false}
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% if showVisibility %}
|
||||
{% set columns = columns|merge({
|
||||
'visible': {'class': 'text-center', 'orderBy': false},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'visible': {'class': 'text-center', 'orderBy': false},
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
|
||||
@@ -40,16 +44,16 @@
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_activity_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<td>{{ widgets.label_color_dot('activity', true, entry.name, null, entry.color) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
{% if entry.project and entry.project.customer %}
|
||||
{# only none-global activities have a project and customer assigned #}
|
||||
{{ widgets.label_customer(entry.project.customer) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||
{# only none-global activities have a project and customer assigned #}
|
||||
{% if entry.project %}
|
||||
{# only none-global activities have a project and customer assigned #}
|
||||
{{ widgets.label_project(entry.project) }}
|
||||
{% if entry.project.customer %}
|
||||
<br>
|
||||
<small>
|
||||
{{ widgets.label_customer(entry.project.customer) }}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||
@@ -58,7 +62,9 @@
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
{% if showVisibility %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
{% endif %}
|
||||
<td class="actions">
|
||||
{{ actions.activity(entry, 'index') }}
|
||||
</td>
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
{% block javascripts %}
|
||||
{# no call to parent(), as we use a custom built for the frontend assets and don't want the default <script> #}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.addEventListener('load', function() {
|
||||
var loader = new KimaiWebLoader(
|
||||
{
|
||||
login: '{{ path('fos_user_security_login') }}',
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if customer.id is not empty %}
|
||||
{% if view != 'details' and is_granted('view', customer) %}
|
||||
{% set actions = actions|merge({'details': path('customer_details', {'id': customer.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('edit', customer) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'edit' %}
|
||||
@@ -25,13 +28,17 @@
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('admin_customer_edit', {'id': customer.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('budget', customer) %}
|
||||
{% set actions = actions|merge({'report': {'url': path('admin_customer_budget', {'id': customer.id})}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('permissions', customer) %}
|
||||
{% set actions = actions|merge({'permissions': {'url': path('admin_customer_permissions', {'id': customer.id})}}) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'permissions' %}
|
||||
{% set class = 'modal-ajax-form' %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'permissions': {'url': path('admin_customer_permissions', {'id': customer.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_project') %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project') %}
|
||||
{% set actions = actions|merge({'project': path('admin_project', {'customer': customer.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_activity') %}
|
||||
@@ -48,12 +55,12 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% if view != 'index' and view != 'custom' %}
|
||||
{% set actions = actions|merge({'back': path('admin_customer')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.customer', {'actions': actions, 'view': view, 'customer': customer}) %}
|
||||
{% if view == 'index' %}
|
||||
{% if view == 'index' or view == 'custom' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "customer/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.customer(customer, 'delete') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set params = {
|
||||
'%activity%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
||||
'%project%': '<strong>' ~ stats.projectAmount ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ customer.name ~ '</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>',
|
||||
'%rate%': '<strong>' ~ stats.recordRate|money ~ '</strong>'
|
||||
} %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_title %}{{ customer.name }}{% endblock %}
|
||||
{% block box_body %}
|
||||
<p>
|
||||
{{ 'admin_customer.short_stats'|trans(params)|raw }}
|
||||
</p>
|
||||
|
||||
{{ progress.progressbar(customer.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(customer.currency) ~ ' / ' ~ customer.budget|money(customer.currency) ) }}
|
||||
{{ progress.progressbar(customer.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ customer.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
174
templates/customer/details.html.twig
Normal file
174
templates/customer/details.html.twig
Normal file
@@ -0,0 +1,174 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "customer/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_actions %}{{ actions.customer(customer, 'details') }}{% endblock %}
|
||||
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% set can_edit = is_granted('edit', customer) %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_attributes %}id="customer_details_box"{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ widgets.label_customer(customer) }}
|
||||
{% if customer.company is not empty %} – {{ customer.company }}{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if can_edit %}
|
||||
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ path('admin_customer_edit', {'id': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if customer.comment is not empty %}
|
||||
{{ customer.comment|comment2html(true) }}
|
||||
{% endif %}
|
||||
<table class="table table-hover">
|
||||
{% if not customer.visible %}
|
||||
<tr>
|
||||
<th>{{ 'label.visible'|trans }}</th>
|
||||
<td>
|
||||
{{ widgets.label_boolean(customer.visible) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.address is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.address'|trans }}</th>
|
||||
<td>
|
||||
{{ customer.address|nl2br }}
|
||||
|
||||
{% if customer.country is not empty %}
|
||||
<br>
|
||||
{{ customer.country|country }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.contact is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.contact'|trans }}</th>
|
||||
<td>{{ customer.contact }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.phone is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.phone'|trans }}</th>
|
||||
<td><a href="tel:{{ customer.phone }}">{{ customer.phone }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.mobile is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.mobile'|trans }}</th>
|
||||
<td><a href="tel:{{ customer.mobile }}">{{ customer.mobile }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.email is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.email'|trans }}</th>
|
||||
<td><a href="mailto:{{ customer.email }}">{{ customer.email }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.homepage is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.homepage'|trans }}</th>
|
||||
<td><a href="{{ customer.homepage }}" target="_blank">{{ customer.homepage|replace({'https://': '', 'http://': ''}) }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.fax is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.fax'|trans }}</th>
|
||||
<td>{{ customer.fax }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.timezone is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.timezone'|trans }}</th>
|
||||
<td><span data-toggle="tooltip" data-placement="top" title="{{ customer.timezone }}">{{ now|date_full(false) }}</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.currency is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.currency'|trans }}</th>
|
||||
<td>{{ customer.currency }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if is_granted('details', customer) %}
|
||||
{% if customer.number is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.number'|trans }}</th>
|
||||
<td>{{ customer.number }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if customer.vatId is not empty %}
|
||||
<tr>
|
||||
<th>{{ 'label.vat_id'|trans }}</th>
|
||||
<td>{{ customer.vatId }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if can_edit %}
|
||||
<tr>
|
||||
<th>{{ 'label.fixedRate'|trans }}</th>
|
||||
<td>
|
||||
{% if customer.fixedRate is not empty %}
|
||||
{{ customer.fixedRate|money(customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
<td>
|
||||
{% if customer.hourlyRate is not empty %}
|
||||
{{ customer.hourlyRate|money(customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for metaField in customer.visibleMetaFields %}
|
||||
<tr>
|
||||
<th>{{ metaField.name }}</th>
|
||||
<td>{{ widgets.form_type_value(metaField.type, metaField.value, customer) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{{ render(controller('App\\Controller\\CustomerController::projectsAction', {'customer': customer.id, 'page': 1})) }}
|
||||
|
||||
{% if stats is not null %}
|
||||
{{ include('customer/embed_budget.html.twig', {'customer': customer, 'stats': stats}) }}
|
||||
{% endif %}
|
||||
|
||||
{% if teams is not null %}
|
||||
{% set options = {'teams': teams, 'team': team} %}
|
||||
{% if is_granted('permissions', customer) %}
|
||||
{% set options = options|merge({'route_create': path('customer_team_create', {'id': customer.id}), 'route_edit': path('admin_customer_permissions', {'id': customer.id})}) %}
|
||||
{% endif %}
|
||||
{{ include('embeds/teams.html.twig', options) }}
|
||||
{% endif %}
|
||||
|
||||
{% if comments is not null %}
|
||||
{% set options = {'form': commentForm, 'comments': comments} %}
|
||||
{% if can_edit %}
|
||||
{% set options = options|merge({'route_pin': 'customer_comment_pin', 'route_delete': 'customer_comment_delete'}) %}
|
||||
{% endif %}
|
||||
{{ include('embeds/comments.html.twig', options) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.customerTeamUpdate kimai.customerUpdate kimai.teamUpdate');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -23,65 +23,60 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ form_row(form.comment) }}
|
||||
<fieldset>
|
||||
{#<legend>Company data</legend>#}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ form_row(form.company) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.vatId) }}
|
||||
</div>
|
||||
{{ form_row(form.address) }}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.company) }}
|
||||
</div>
|
||||
{{ form_row(form.address) }}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.country) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.currency) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.timezone) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.number) }}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
{#<legend>Contact data</legend>#}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.contact) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.email) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.homepage) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.vatId) }}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.mobile) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.phone) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.fax) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.country) }}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
{#<legend>Financial data</legend>#}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.fixedRate) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.hourlyRate) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.currency) }}
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.timezone) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.contact) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.email) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.homepage) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.mobile) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.phone) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.fax) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.fixedRate) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.hourlyRate) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.budget is defined %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.budget) }}
|
||||
@@ -90,14 +85,10 @@
|
||||
{{ form_row(form.timeBudget) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{{ form_row(form.visible) }}
|
||||
{% if form.metaFields is defined and form.metaFields is not empty %}
|
||||
<fieldset>
|
||||
{#<legend>Custom fields</legend>#}
|
||||
{{ form_row(form.metaFields) }}
|
||||
</fieldset>
|
||||
{{ form_row(form.metaFields) }}
|
||||
{% endif %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
|
||||
22
templates/customer/embed_budget.html.twig
Normal file
22
templates/customer/embed_budget.html.twig
Normal file
@@ -0,0 +1,22 @@
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'customer': customer, 'stats': stats} %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_title %}{{ 'label.budget'|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="budget_box"{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set params = {
|
||||
'%activity%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
||||
'%project%': '<strong>' ~ stats.projectAmount ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ customer.name ~ '</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>',
|
||||
'%rate%': '<strong>' ~ stats.recordRate|money ~ '</strong>'
|
||||
} %}
|
||||
|
||||
<p>
|
||||
{{ 'admin_customer.short_stats'|trans(params)|raw }}
|
||||
</p>
|
||||
|
||||
{{ progress.progressbar(customer.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(customer.currency) ~ ' / ' ~ customer.budget|money(customer.currency) ) }}
|
||||
{{ progress.progressbar(customer.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ customer.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
30
templates/customer/embed_projects.html.twig
Normal file
30
templates/customer/embed_projects.html.twig
Normal file
@@ -0,0 +1,30 @@
|
||||
{% if projects|length > 0 %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'customer': customer, 'projects': projects, 'page': page} %}
|
||||
{% import "project/actions.html.twig" as actions %}
|
||||
{% block box_title %}{{ 'menu.admin_project'|trans }}{% endblock %}
|
||||
{% block box_attributes %}
|
||||
id="project_list_box" data-href="{{ path('customer_projects', {'id': customer.id}) }}" data-reload="kimai.projectUpdate"
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{{ pagerfanta(projects, 'twitter_bootstrap3_translated', { proximity: 1, css_container_class: 'pagination pagination-sm inline', routeName: 'customer_projects', routeParams: {'id': customer.id} }) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}no-padding{% endblock %}
|
||||
{% block box_tools_attributes %}data-page="{{ page }}"{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-hover dataTable">
|
||||
<tbody>
|
||||
{% for project in projects %}
|
||||
<tr>
|
||||
<td>{{ project.name }}</td>
|
||||
<td>{{ project.comment|comment2html }}</td>
|
||||
<td class="actions">{{ actions.project(project, 'custom') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
<script type="text/javascript">
|
||||
KimaiPaginatedBoxWidget.create('#project_list_box');
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -4,11 +4,10 @@
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "customer/actions.html.twig" as actions %}
|
||||
|
||||
{% set showVisibility = query.visibility != 1 %}
|
||||
{% set columns = {
|
||||
'name': 'alwaysVisible',
|
||||
'comment': 'hidden-xs',
|
||||
'country': 'hidden-xs hidden-sm',
|
||||
'number': 'hidden-xs',
|
||||
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
|
||||
} %}
|
||||
{% for field in metaColumns %}
|
||||
{% set columns = columns|merge({
|
||||
@@ -16,12 +15,14 @@
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'email': {'class': 'text-center hidden hidden-xs', 'orderBy': false},
|
||||
'homepage': {'class': 'text-center hidden hidden-xs', 'orderBy': false},
|
||||
'mobile': {'class': 'text-center hidden hidden-xs', 'orderBy': false},
|
||||
'phone': {'class': 'text-center hidden hidden-xs', 'orderBy': false},
|
||||
'team': {'class': 'text-center', 'orderBy': false},
|
||||
'visible': {'class': 'text-center', 'orderBy': false},
|
||||
}) %}
|
||||
{% if showVisibility %}
|
||||
{% set columns = columns|merge({
|
||||
'visible': {'class': 'text-center', 'orderBy': false},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
|
||||
@@ -40,47 +41,21 @@
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.customerUpdate'}) }}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.customerUpdate kimai.customerTeamUpdate'}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_customer_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<td>{{ widgets.label_color_dot('customer', true, entry.name, null, entry.color) }} {% if entry.company is not empty %}({{ entry.company }}){% endif %}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td>
|
||||
<tr class="alternative-link open-edit" data-href="{{ path('customer_details', {'id': entry.id}) }}">
|
||||
<td>
|
||||
{{ widgets.label_color_dot('customer', true, entry.name, null, entry.color) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">
|
||||
{{ entry.comment|comment2html }}
|
||||
</td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">
|
||||
{% if entry.email is not empty %}
|
||||
<a href="mailto:{{ entry.email }}"><i class="{{ 'mail'|icon }}"></i></a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'homepage') }}">
|
||||
{% if entry.homepage is not empty %}
|
||||
<a href="{{ entry.homepage }}" target="_blank"><i class="{{ 'home'|icon }}"></i></a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'mobile') }}">
|
||||
{% if entry.mobile is not empty %}
|
||||
<a href="tel:{{ entry.mobile }}"><i class="{{ 'mobile'|icon }}"></i></a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'phone') }}">
|
||||
{% if entry.phone is not empty %}
|
||||
<a href="tel:{{ entry.phone }}"><i class="{{ 'phone'|icon }}"></i></a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">
|
||||
{% if entry.teams|length > 0 %}
|
||||
{{ widgets.badge_counter(entry.teams|length) }}
|
||||
@@ -88,7 +63,9 @@
|
||||
{{ widgets.icon('unlocked') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
{% if showVisibility %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
{% endif %}
|
||||
<td class="actions">
|
||||
{{ actions.customer(entry, 'index') }}
|
||||
</td>
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
<div class="box box-solid box-danger">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">{{ title|default('confirm.delete'|trans) }}</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
{{ message|raw }}
|
||||
</div>
|
||||
{{ form_start(form) }}
|
||||
<div class="box-body">
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'danger'} %}
|
||||
{% block box_before %}
|
||||
{{ form_start(form) }}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ title|default('confirm.delete'|trans) }}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% block form_body %}
|
||||
{{ form_widget(form) }}
|
||||
{% if used is same as (false) %}
|
||||
{{ 'delete.not_in_use'|trans }}
|
||||
<div class="hidden">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ message|raw }}</p>
|
||||
{{ form_widget(form) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{% endblock %}
|
||||
{% block box_footer %}
|
||||
<input type="submit" value="{{ 'action.delete'|trans }}" class="btn btn-danger" />
|
||||
{% if back %}
|
||||
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block box_after %}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
53
templates/embeds/comments.html.twig
Normal file
53
templates/embeds/comments.html.twig
Normal file
@@ -0,0 +1,53 @@
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'form': form, 'comments': comments, 'route_pin': route_pin|default(null), 'route_delete': route_delete|default(null)} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ 'label.comment'|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="comments_box"{% endblock %}
|
||||
{% block box_body_class %}box-comments{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set replacer = {} %}
|
||||
{% for pref in app.user.preferences %}
|
||||
{% set replacer = replacer|merge({('{{user.' ~ pref.name ~ '}}'): pref.value}) %}
|
||||
{% endfor %}
|
||||
{% if comments|length == 0 %}
|
||||
{{ 'error.no_comments_found'|trans }}
|
||||
{% endif %}
|
||||
{% for comment in comments %}
|
||||
<div class="box-comment">
|
||||
{{ widgets.user_avatar(comment.createdBy, false, 'img-sm') }}
|
||||
<div class="comment-text">
|
||||
<span class="username">
|
||||
{{ widgets.username(comment.createdBy) }}
|
||||
<span class="text-muted pull-right">
|
||||
{% if route_pin is not null %}
|
||||
<a href="{{ path(route_pin, {'id': comment.id}) }}" class="btn btn-default btn-xs {% if comment.pinned %}active{% endif %}"><i class="{{ 'pin'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% if route_delete is not null %}
|
||||
<a href="{{ path(route_delete, {'id': comment.id}) }}" class="confirmation-link btn btn-default btn-xs" data-method="POST" data-question="confirm.delete" data-msg-error="action.delete.error" data-msg-success="action.delete.success"><i class="{{ 'delete'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{{ comment.createdAt|date_full }}
|
||||
</span>
|
||||
</span>
|
||||
{{ comment.message|replace(replacer)|md2html }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% block box_footer -%}
|
||||
{% if form is not null %}
|
||||
{{ form_start(form) }}
|
||||
<div class="input-group">
|
||||
{{ widgets.user_avatar(app.user, false, 'img-responsive img-sm') }}
|
||||
<div class="img-push">
|
||||
{{ form_widget(form.message) }}
|
||||
</div>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-default">
|
||||
{{ 'action.save'|trans }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{{ form_widget(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endif %}
|
||||
{%- endblock %}
|
||||
{% endembed %}
|
||||
32
templates/embeds/teams.html.twig
Normal file
32
templates/embeds/teams.html.twig
Normal file
@@ -0,0 +1,32 @@
|
||||
{%
|
||||
set options = {
|
||||
'teams': teams,
|
||||
'team': team|default(null),
|
||||
'route_create': route_create|default(null),
|
||||
'route_edit': route_edit|default(null),
|
||||
'empty_message': empty_message|default('team.visibility_global')
|
||||
}
|
||||
%}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with options %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_tools %}
|
||||
{% if teams|length == 0 and team is null and route_create is not null and is_granted('create_team') %}
|
||||
<a class="btn-box-tool" href="{{ route_create }}" data-toggle="tooltip" data-placement="top" title="{{ 'team.create_default'|trans({}, 'teams') }}"><i class="{{ 'create'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% if route_edit is not null %}
|
||||
<a class="modal-ajax-form open-edit btn btn-box-tool" href="{{ route_edit }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_attributes %}id="team_listing_box"{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ 'permissions'|trans({}, 'actions') }}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if teams|length > 0 %}
|
||||
<p>{{ 'team.visibility_restricted'|trans({}, 'teams') }}</p>
|
||||
{{ widgets.team_list(teams) }}
|
||||
{% else %}
|
||||
<p>{{ empty_message|default('team.visibility_global')|trans({}, 'teams') }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
@@ -15,7 +15,17 @@
|
||||
{% import _self as macro %}
|
||||
<div class="breadcrumb">
|
||||
<div class="box-tools">
|
||||
{{ macro.table_actions(tools) }}
|
||||
<div class="btn-group">
|
||||
{% set actions = {} %}
|
||||
{%- for icon, values in tools %}
|
||||
{% if icon == 'back' %}
|
||||
{{ macro.action_button(icon, values) }}
|
||||
{% else %}
|
||||
{% set actions = actions|merge({(icon): values}) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ macro.table_actions(actions, '') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
@@ -69,11 +79,10 @@
|
||||
|
||||
{% macro user_avatar(user, tooltip, class) %}
|
||||
{% set avatar = avatar(user, admin_lte_context.default_avatar) %}
|
||||
{% set showTooltip = tooltip|default(true) %}
|
||||
{% if tooltip is not defined or tooltip is same as (false)%}
|
||||
<img src="{{ avatar }}" class="{{ class|default('img-circle') }}" alt="{{ user.displayName }}" />
|
||||
{% if tooltip is same as (false) %}
|
||||
<img src="{{ avatar }}" class="img-circle{% if class is not empty %} {{ class }}{% endif %}" alt="{{ user.displayName }}" />
|
||||
{% else %}
|
||||
<img src="{{ avatar }}" class="{{ class|default('img-circle') }}" data-toggle="tooltip" data-placement="top" alt="{{ user.displayName }}" title="{{ user.displayName }}" />
|
||||
<img src="{{ avatar }}" class="img-circle{% if class is not empty %} {{ class }}{% endif %}" data-toggle="tooltip" data-placement="top" alt="{{ user.displayName }}" title="{{ tooltip|default(user.displayName) }}" />
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -200,30 +209,50 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table_actions(actions) %}
|
||||
{% macro table_actions(actions, class) %}
|
||||
{%- import _self as macro -%}
|
||||
{% if actions|length >= 1 %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">{{ 'label.actions'|trans }}
|
||||
<span class="fa fa-caret-down"></span></button>
|
||||
{% if class is null %}
|
||||
{% set class = 'btn-sm' %}
|
||||
{% endif %}
|
||||
{% set trash = null %}
|
||||
{% set divider = false %}
|
||||
<div class="btn-group dropdown">
|
||||
<button type="button" class="btn btn-default {{ class }} dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fa fa-chevron-down"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
{%- apply spaceless -%}
|
||||
{%- for icon,values in actions %}
|
||||
{% set class = '' %}
|
||||
{% if icon == 'trash' %}
|
||||
{% set class = 'delete' %}
|
||||
{% if actions|length > 1 %}
|
||||
<li class="divider"></li>
|
||||
{% if icon == 'divider' and values is null %}
|
||||
{% if not loop.last and divider is same as (false) %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
{% set divider = true %}
|
||||
{% else %}
|
||||
{% if values is iterable %}
|
||||
{% set values = values|merge({'title': icon|trans({}, 'actions')}) %}
|
||||
{% else %}
|
||||
{% set values = {'url': values, 'title': icon|trans({}, 'actions')} %}
|
||||
{% endif %}
|
||||
{% if icon == 'trash' %}
|
||||
{% set trash = values %}
|
||||
{% else %}
|
||||
{% set divider = false %}
|
||||
<li>
|
||||
{{ macro.action_button(icon, values, false) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li class="{{ class }}">
|
||||
{% if values is iterable %}
|
||||
{{ macro.action_button(icon, values|merge({'title': icon|trans({}, 'actions')}), false) }}
|
||||
{% else %}
|
||||
{{ macro.action_button(icon, {'url': values, 'title': icon|trans({}, 'actions')}, false) }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor -%}
|
||||
{%- if trash is not null %}
|
||||
{% if actions|length > 1 and divider is same as (false) %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li class="delete">
|
||||
{{ macro.action_button('trash', trash, false) }}
|
||||
</li>
|
||||
{% endif -%}
|
||||
{% endapply %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -371,3 +400,49 @@
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro team_list(teams, showTitle) %}
|
||||
{% if showTitle is null %}
|
||||
{% set showTitle = true %}
|
||||
{% endif %}
|
||||
{% import _self as macro %}
|
||||
<table class="table table-hover dataTable" role="grid">
|
||||
{% if showTitle %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.team'|trans }}</th>
|
||||
<th>{{ 'label.user'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% endif %}
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr{% if is_granted('edit', team) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_team_member', {'id': team.id}) }}"{% endif %}>
|
||||
<td>
|
||||
{{ team.name }}
|
||||
</td>
|
||||
<td class="avatars">
|
||||
{% set userTeamCount = team.users|length %}
|
||||
{{ macro.user_avatar(team.teamlead, ('label.teamlead'|trans ~ ': ' ~ team.teamlead.displayName), 'teamlead') }}
|
||||
{% set teamHiddenId = 'team_' ~ team.id ~ '_hiddenUser' ~ random() %}
|
||||
{% set counter = 0 %}
|
||||
{% for user in team.users %}
|
||||
{% if user != team.teamlead %}
|
||||
{{ macro.user_avatar(user) }}
|
||||
{% set counter = counter + 1 %}
|
||||
{% endif %}
|
||||
{% if userTeamCount > 5 and counter == 4 and not loop.last %}
|
||||
<a href="#" onclick="$('#{{ teamHiddenId }}').toggleClass('hidden');$(this).hide();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - 5)}) }}</a>
|
||||
<span class="hidden" id="{{ teamHiddenId }}">
|
||||
{% set counter = counter + 1 %}
|
||||
{% endif %}
|
||||
{% if userTeamCount > 5 and counter != 4 and loop.last %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{% set actions = {} %}
|
||||
|
||||
{% if project.id is not empty %}
|
||||
{% if view != 'details' and is_granted('view', project) %}
|
||||
{% set actions = actions|merge({'details': path('project_details', {'id': project.id})}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('edit', project) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'edit' %}
|
||||
@@ -25,11 +28,15 @@
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'edit': {'url': path('admin_project_edit', {'id': project.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('budget', project) %}
|
||||
{% set actions = actions|merge({'report': {'url': path('admin_project_budget', {'id': project.id})}}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('permissions', project) %}
|
||||
{% set actions = actions|merge({'permissions': {'url': path('admin_project_permissions', {'id': project.id})}}) %}
|
||||
{% set class = '' %}
|
||||
{% if view != 'permissions' %}
|
||||
{% set class = 'modal-ajax-form' %}
|
||||
{% endif %}
|
||||
{% set actions = actions|merge({'permissions': {'url': path('admin_project_permissions', {'id': project.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_activity') %}
|
||||
{% set actions = actions|merge({'activity': path('admin_activity', {'customer': project.customer.id, 'project': project.id})}) %}
|
||||
@@ -45,12 +52,12 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if view != 'index' %}
|
||||
{% if view != 'index' and view != 'custom' %}
|
||||
{% set actions = actions|merge({'back': path('admin_project')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set event = trigger('actions.project', {'actions': actions, 'view': view, 'project': project}) %}
|
||||
{% if view == 'index' %}
|
||||
{% if view == 'index' or view == 'custom' %}
|
||||
{{ widgets.table_actions(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "project/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.project(project, 'delete') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set params = {
|
||||
'%project%': '<strong>' ~ project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ project.customer.name ~ '</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%activities%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
||||
} %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_title %}{{ project.name }}{% endblock %}
|
||||
{% block box_body %}
|
||||
<p>
|
||||
{{ 'admin_project.short_stats'|trans(params)|raw }}
|
||||
</p>
|
||||
|
||||
{{ progress.progressbar(project.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(project.customer.currency) ~ ' / ' ~ project.budget|money(project.customer.currency) ) }}
|
||||
{{ progress.progressbar(project.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ project.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
151
templates/project/details.html.twig
Normal file
151
templates/project/details.html.twig
Normal file
@@ -0,0 +1,151 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "project/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.project(project, 'details') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% set can_edit = is_granted('edit', project) %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "customer/actions.html.twig" as customerActions %}
|
||||
{% block box_attributes %}id="project_details_box"{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if can_edit %}
|
||||
<a class="modal-ajax-form open-edit btn btn-box-tool" data-href="{{ path('admin_project_edit', {'id': project.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ widgets.label_project(project) }}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if project.comment is not empty %}
|
||||
{{ project.comment|comment2html(true) }}
|
||||
{% endif %}
|
||||
<table class="table table-hover">
|
||||
{% if not project.visible %}
|
||||
<tr>
|
||||
<th>{{ 'label.visible'|trans }}</th>
|
||||
<td>
|
||||
{{ widgets.label_boolean(project.visible) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{{ 'label.customer'|trans }}</th>
|
||||
<td>
|
||||
{{ widgets.label_customer(project.customer) }}
|
||||
{% if project.customer.teams|length == 0 %}
|
||||
{{ widgets.icon('unlocked') }}
|
||||
{% endif %}
|
||||
|
||||
{{ customerActions.customer(project.customer, 'custom') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if is_granted('details', project) %}
|
||||
<tr>
|
||||
<th>{{ 'label.orderNumber'|trans }}</th>
|
||||
<td>
|
||||
{{ project.orderNumber }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.orderDate'|trans }}</th>
|
||||
<td>
|
||||
{% if project.orderDate is not empty %}
|
||||
{{ project.orderDate|date_full }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.project_start'|trans }}</th>
|
||||
<td>
|
||||
{% if project.start is not empty %}
|
||||
{{ project.start|date_full }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.project_end'|trans }}</th>
|
||||
<td>
|
||||
{% if project.end is not empty %}
|
||||
{{ project.end|date_full }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if can_edit %}
|
||||
<tr>
|
||||
<th>{{ 'label.fixedRate'|trans }}</th>
|
||||
<td>
|
||||
{% if project.fixedRate is not empty %}
|
||||
{{ project.fixedRate|money(project.customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
<td>
|
||||
{% if project.hourlyRate is not empty %}
|
||||
{{ project.hourlyRate|money(project.customer.currency) }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for metaField in project.visibleMetaFields %}
|
||||
<tr>
|
||||
<th>{{ metaField.name }}</th>
|
||||
<td>{{ widgets.form_type_value(metaField.type, metaField.value, project) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{{ render(controller('App\\Controller\\ProjectController::activitiesAction', {'project': project.id, 'page': 1})) }}
|
||||
|
||||
{% if stats is not null %}
|
||||
{{ include('project/embed_budget.html.twig', {'project': project, 'stats': stats}) }}
|
||||
{% endif %}
|
||||
|
||||
{% if teams is not null%}
|
||||
{% set options = {'teams': teams, 'team': team} %}
|
||||
{% if is_granted('permissions', project) %}
|
||||
{% set options = options|merge({'route_create': path('project_team_create', {'id': project.id}), 'route_edit': path('admin_project_permissions', {'id': project.id})}) %}
|
||||
{% endif %}
|
||||
{% if project.customer.teams|length > 0 %}
|
||||
{% set options = options|merge({'empty_message': 'team.project_visibility_inherited'}) %}
|
||||
{% endif %}
|
||||
{{ include('embeds/teams.html.twig', options) }}
|
||||
{% endif %}
|
||||
|
||||
{% if comments is not null %}
|
||||
{% set options = {'form': commentForm, 'comments': comments} %}
|
||||
{% if can_edit %}
|
||||
{% set options = options|merge({'route_pin': 'project_comment_pin', 'route_delete': 'project_comment_delete'}) %}
|
||||
{% endif %}
|
||||
{{ include('embeds/comments.html.twig', options) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.projectTeamUpdate kimai.projectUpdate kimai.teamUpdate kimai.customerUpdate ');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
29
templates/project/embed_activities.html.twig
Normal file
29
templates/project/embed_activities.html.twig
Normal file
@@ -0,0 +1,29 @@
|
||||
{% if activities|length > 0 %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'project': project, 'activities': activities, 'page': page} %}
|
||||
{% import "activity/actions.html.twig" as actions %}
|
||||
{% block box_title %}{{ 'menu.admin_activity'|trans }}{% endblock %}
|
||||
{% block box_attributes %}
|
||||
id="activity_list_box" data-href="{{ path('project_activities', {'id': project.id}) }}" data-reload="kimai.activityUpdate"
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{{ pagerfanta(activities, 'twitter_bootstrap3_translated', { proximity: 1, css_container_class: 'pagination pagination-sm inline', routeName: 'project_activities', routeParams: {'id': project.id} }) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}no-padding{% endblock %}
|
||||
{% block box_tools_attributes %}data-page="{{ page }}"{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-hover dataTable">
|
||||
<tbody>
|
||||
{% for activity in activities %}
|
||||
<tr>
|
||||
<td>{{ activity.name }}</td>
|
||||
<td class="actions">{{ actions.activity(activity, 'custom') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
<script type="text/javascript">
|
||||
KimaiPaginatedBoxWidget.create('#activity_list_box');
|
||||
</script>
|
||||
{% endif %}
|
||||
21
templates/project/embed_budget.html.twig
Normal file
21
templates/project/embed_budget.html.twig
Normal file
@@ -0,0 +1,21 @@
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% block box_title %}{{ 'label.budget'|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="budget_box"{% endblock %}
|
||||
{% block box_body %}
|
||||
{% set params = {
|
||||
'%project%': '<strong>' ~ project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ project.customer.name ~ '</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%activities%': '<strong>' ~ stats.activityAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
||||
} %}
|
||||
|
||||
<p>
|
||||
{{ 'admin_project.short_stats'|trans(params)|raw }}
|
||||
</p>
|
||||
|
||||
{{ progress.progressbar(project.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(project.customer.currency) ~ ' / ' ~ project.budget|money(project.customer.currency) ) }}
|
||||
{{ progress.progressbar(project.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ project.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
@@ -4,12 +4,11 @@
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "project/actions.html.twig" as actions %}
|
||||
|
||||
{% set showVisibility = query.visibility != 1 %}
|
||||
{% set columns = {
|
||||
'name': 'alwaysVisible',
|
||||
'customer': 'hidden-xs',
|
||||
'comment': 'hidden-xs hidden-sm',
|
||||
'orderNumber': 'hidden-xs hidden-sm',
|
||||
'orderDate': 'hidden-xs hidden-sm',
|
||||
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
|
||||
} %}
|
||||
{% for field in metaColumns %}
|
||||
{% set columns = columns|merge({
|
||||
@@ -18,7 +17,13 @@
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'team': {'class': 'text-center', 'orderBy': false},
|
||||
'visible': {'class': 'text-center', 'orderBy': false},
|
||||
}) %}
|
||||
{% if showVisibility %}
|
||||
{% set columns = columns|merge({
|
||||
'visible': {'class': 'text-center', 'orderBy': false},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
|
||||
@@ -37,21 +42,15 @@
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.projectUpdate'}) }}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.projectUpdate kimai.projectTeamUpdate'}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_project_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<tr class="alternative-link open-edit" data-href="{{ path('project_details', {'id': entry.id}) }}">
|
||||
<td>{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
{{ widgets.label_customer(entry.customer) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">
|
||||
{% if entry.orderDate is not empty %}
|
||||
{{ entry.orderDate|date_short }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html() }}</td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
@@ -64,7 +63,9 @@
|
||||
{{ widgets.icon('unlocked') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
{% if showVisibility %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
{% endif %}
|
||||
<td class="actions">
|
||||
{{ actions.project(entry, 'index') }}
|
||||
</td>
|
||||
|
||||
14
templates/team/edit_member.html.twig
Normal file
14
templates/team/edit_member.html.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "team/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'teams.title'|trans({}, 'teams') }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.team(team, 'edit') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||
'title': team.name|default('create'|trans),
|
||||
'form': form,
|
||||
'back': path('admin_team')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -46,6 +46,10 @@
|
||||
{% set actions = actions|merge({'edit': {'url': path('admin_timesheet_edit', {'id': timesheet.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if view == 'index' and is_granted('delete', timesheet) %}
|
||||
{% set actions = actions|merge({'trash': {'url': path('delete_timesheet', {'id' : timesheet.id}), 'class': 'api-link', 'attr': {'data-event': 'kimai.timesheetDelete kimai.timesheetUpdate', 'data-method': 'DELETE', 'data-question': 'confirm.delete', 'data-msg-error': 'action.delete.error', 'data-msg-success': 'action.delete.success'}}}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
{% set actions = actions|merge({'edit': {'url': path('timesheet_edit', {'id': timesheet.id}), 'class': class}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if view == 'index' and is_granted('delete', timesheet) %}
|
||||
{% set actions = actions|merge({'trash': {'url': path('delete_timesheet', {'id' : timesheet.id}), 'class': 'api-link', 'attr': {'data-event': 'kimai.timesheetDelete kimai.timesheetUpdate', 'data-method': 'DELETE', 'data-question': 'confirm.delete', 'data-msg-error': 'action.delete.error', 'data-msg-success': 'action.delete.success'}}}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
{% if is_granted('preferences', user) %}
|
||||
{% set actions = actions|merge({'settings': {'url': path('user_profile_preferences', {'username' : user.username})}}) %}
|
||||
{% endif %}
|
||||
{% if actions|length > 0 %}
|
||||
{% set actions = actions|merge({'divider': null}) %}
|
||||
{% endif %}
|
||||
{% if is_granted('view_other_timesheet') and user.enabled %}
|
||||
{% set actions = actions|merge({'timesheet': path('admin_timesheet', {'users': [user.id]})}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -4,21 +4,28 @@
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% import "user/actions.html.twig" as actions %}
|
||||
|
||||
{% set showVisibility = query.visibility != 1 %}
|
||||
{% set columns = {
|
||||
'alias': 'alwaysVisible',
|
||||
'username': 'hidden-xs',
|
||||
'email': 'hidden-xs hidden-md hidden-sm',
|
||||
'title': 'hidden-xs hidden-sm',
|
||||
} %}
|
||||
{% set columns = columns|merge({
|
||||
'roles': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||
}) %}
|
||||
{% for pref in preferences %}
|
||||
{% set columns = columns|merge({
|
||||
('mf_' ~ pref.name): {'title': pref.label, 'class': 'hidden-xs hidden-sm', 'orderBy': false}
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'roles': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||
'team': {'class': 'hidden-xs', 'orderBy': false},
|
||||
'active': {'class': '', 'orderBy': false},
|
||||
'team': {'class': 'text-center hidden-xs', 'orderBy': false},
|
||||
}) %}
|
||||
{% if showVisibility %}
|
||||
{% set columns = columns|merge({
|
||||
'active': {'class': '', 'orderBy': false},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
|
||||
@@ -40,19 +47,9 @@
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.userUpdate'}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="open-edit alternative-link" data-href="{{ path('user_profile_edit', {'username': entry.username}) }}"{% endif %}>
|
||||
<tr{% if is_granted('view', entry) %} class="open-edit alternative-link" data-href="{{ path('user_profile', {'username': entry.username}) }}"{% endif %}>
|
||||
<td>{{ widgets.user_avatar(entry) }} {{ widgets.username(entry) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">{{ entry.username }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
|
||||
{% for pref in preferences %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ pref.name) }}">
|
||||
{% set metaField = entry.preference(pref.name) %}
|
||||
{% if not metaField is null and metaField.value is not null and metaField.value is not empty %}
|
||||
{{ widgets.form_type_value(pref.type, metaField.value, entry) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'roles') }}">
|
||||
{% set showUserRole = entry.roles|length == 1 %}
|
||||
{% for role in entry.roles %}
|
||||
@@ -61,16 +58,22 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">
|
||||
{% for team in entry.teams %}
|
||||
{% if entry.isTeamleadOf(team) %}
|
||||
{{ widgets.label_team(team, 'success') }}
|
||||
{% else %}
|
||||
{{ widgets.label_team(team) }}
|
||||
{% for pref in preferences %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ pref.name) }}">
|
||||
{% set metaField = entry.preference(pref.name) %}
|
||||
{% if not metaField is null and metaField.value is not null and metaField.value is not empty %}
|
||||
{{ widgets.form_type_value(pref.type, metaField.value, entry) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="text-center {{ tables.data_table_column_class(tableName, columns, 'team') }}">
|
||||
{% if entry.teams|length > 0 %}
|
||||
{{ widgets.badge_counter(entry.teams|length) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'active') }}">{{ widgets.label_visible(entry.enabled) }}</td>
|
||||
{% if showVisibility %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'active') }}">{{ widgets.label_visible(entry.enabled) }}</td>
|
||||
{% endif %}
|
||||
<td class="actions">
|
||||
{{ actions.user(entry, 'index') }}
|
||||
</td>
|
||||
|
||||
@@ -6,81 +6,104 @@
|
||||
{% block page_actions %}{{ actions.user(user, tab) }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% import _self as macro %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
|
||||
<div class="col-md-8">
|
||||
{% block profile_content %}{% endblock %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
{{ macro.profile_box(user, stats) }}
|
||||
{{ macro.profile_infos(user, stats) }}
|
||||
<div class="col-md-4">
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_body_class %}box-profile{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="text-center">
|
||||
{{ widgets.user_avatar(user) }}
|
||||
<h3 class="profile-username">{{ widgets.username(user) }}</h3>
|
||||
<p class="text-muted">{{ user.title }}</p>
|
||||
</div>
|
||||
{% set seeOwnRate = is_granted('view_rate_own_timesheet') %}
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{{ 'stats.durationMonth'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.durationThisMonth|duration }}</td>
|
||||
</tr>
|
||||
{% if seeOwnRate %}
|
||||
<tr>
|
||||
<th>{{ 'stats.amountMonth'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.amountThisMonth|money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{{ 'stats.durationTotal'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.durationTotal|duration }}</td>
|
||||
</tr>
|
||||
{% if seeOwnRate %}
|
||||
<tr>
|
||||
<th>{{ 'stats.amountTotal'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.amountTotal|money }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_title %}{{ 'profile.about_me'|trans }}{% endblock %}
|
||||
{% block box_body %}
|
||||
{# colors = purple, blue, aqua, red, green #}
|
||||
<table class="table no-border">
|
||||
<tr>
|
||||
<th>{{ 'label.id'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'label.username'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'profile.first_entry'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.firstEntry|date_short }}</td>
|
||||
</tr>
|
||||
{% if is_granted('hourly-rate', user) %}
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.preferenceValue('hourly_rate') }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% if user.teams is not empty and (is_granted('teams', user) or (app.user == user and is_granted('view_team_member'))) %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ 'label.my_teams'|trans }}{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if is_granted('roles', user) %}
|
||||
<a class="btn-box-tool" href="{{ path('user_profile_teams', {'username': user.username}) }}"><i class="{{ 'edit'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ widgets.team_list(user.teams) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro profile_infos(user, stats) %}
|
||||
<div class="box box-{{ admin_lte_context.widget.type }}">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ 'profile.about_me'|trans }}</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<ul class="nav nav-stacked">
|
||||
{# colors = purple, blue, aqua, red, green #}
|
||||
<li><a href="#">{{ 'label.id'|trans }} <span class="pull-right badge bg-aqua">{{ user.id }}</span></a></li>
|
||||
<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.hourlyRate'|trans }} <span class="pull-right badge bg-blue">{{ user.preferenceValue('hourly_rate') }}</span></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.teamUpdate', true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% macro profile_box(user, stats) %}
|
||||
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
<div class="box box-{{ admin_lte_context.widget.type }}">
|
||||
<div class="box-body box-profile">
|
||||
<div class="text-center">
|
||||
{{ widgets.user_avatar(user) }}
|
||||
<h3 class="profile-username">{{ widgets.username(user) }}</h3>
|
||||
<p class="text-muted">{{ user.title }}</p>
|
||||
</div>
|
||||
|
||||
<ul class="list-group list-group-unbordered">
|
||||
|
||||
<li class="list-group-item">
|
||||
<b>{{ 'stats.durationMonth'|trans }}</b> <a class="pull-right">{{ stats.durationThisMonth|duration }}</a>
|
||||
</li>
|
||||
|
||||
{% if is_granted('view_rate_own_timesheet') %}
|
||||
<li class="list-group-item">
|
||||
<b>{{ 'stats.amountMonth'|trans }}</b> <a class="pull-right">{{ stats.amountThisMonth|money }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="list-group-item">
|
||||
<b>{{ 'stats.durationTotal'|trans }}</b> <a class="pull-right">{{ stats.durationTotal|duration }}</a>
|
||||
</li>
|
||||
|
||||
{% if is_granted('view_rate_own_timesheet') %}
|
||||
<li class="list-group-item">
|
||||
<b>{{ 'stats.amountTotal'|trans }}</b> <a class="pull-right">{{ stats.amountTotal|money }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
|
||||
{% macro profile_list_unused(user, items) %}
|
||||
|
||||
@@ -53,8 +53,6 @@
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.userRoleUpdate', function() {
|
||||
document.location.reload();
|
||||
});
|
||||
KimaiReloadPageWidget.create('kimai.userRoleUpdate');
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,99 +1,39 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box">
|
||||
{% if not title is empty %}
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ title|trans }}</h3>
|
||||
{#
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-wrench"></i></button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#">Action</a></li>
|
||||
<li><a href="#">Another action</a></li>
|
||||
<li><a href="#">Something else here</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#">Separated link</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button>
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="box-body">
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_title %}
|
||||
{% if not title is empty %}{{ title|trans }}{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ render_widget(widgets.0) }}
|
||||
</div>
|
||||
{#
|
||||
<div class="col-md-4">
|
||||
<p class="text-center">
|
||||
<strong>Goal Completion</strong>
|
||||
</p>
|
||||
<div class="progress-group">
|
||||
<span class="progress-text">Add Products to Cart</span>
|
||||
<span class="progress-number"><b>160</b>/200</span>
|
||||
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-aqua" style="width: 80%"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block box_footer %}
|
||||
{% if widgets|length > 1 %}
|
||||
<div class="row">
|
||||
{% set width = (widgets|length) - 1 %}
|
||||
{% set rawWidth = 12 / width %}
|
||||
{% set columnWidth = rawWidth|round(0, 'floor') %}
|
||||
{% for widget in widgets|slice(1, width) %}
|
||||
{% set data = widget.data %}
|
||||
{% if widget.option('dataType') == 'duration' %}
|
||||
{% set data = widget.data|duration %}
|
||||
{% elseif widget.option('dataType') == 'money' %}
|
||||
{% set data = widget.data|money %}
|
||||
{% endif %}
|
||||
<div class="col-sm-{{ columnWidth }} col-xs-{{ columnWidth * 2 }}">
|
||||
<div class="description-block border-right">
|
||||
<h5 class="description-header">{{ data }}</h5>
|
||||
<span class="description-text">{{ widget.title|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-group">
|
||||
<span class="progress-text">Complete Purchase</span>
|
||||
<span class="progress-number"><b>310</b>/400</span>
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-red" style="width: 80%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-group">
|
||||
<span class="progress-text">Visit Premium Page</span>
|
||||
<span class="progress-number"><b>480</b>/800</span>
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-green" style="width: 80%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-group">
|
||||
<span class="progress-text">Send Inquiries</span>
|
||||
<span class="progress-number"><b>250</b>/500</span>
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-yellow" style="width: 80%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
{% if widgets|length > 1 %}
|
||||
<div class="box-footer">
|
||||
<div class="row">
|
||||
{% set width = (widgets|length) - 1 %}
|
||||
{% set rawWidth = 12 / width %}
|
||||
{% set columnWidth = rawWidth|round(0, 'floor') %}
|
||||
{% for widget in widgets|slice(1, width) %}
|
||||
{% set data = widget.data %}
|
||||
{% if widget.option('dataType') == 'duration' %}
|
||||
{% set data = widget.data|duration %}
|
||||
{% elseif widget.option('dataType') == 'money' %}
|
||||
{% set data = widget.data|money %}
|
||||
{% endif %}
|
||||
<div class="col-sm-{{ columnWidth }} col-xs-{{ columnWidth * 2 }}">
|
||||
<div class="description-block border-right">
|
||||
{#<span class="description-percentage text-green"><i class="fa fa-caret-up"></i> 17%</span>#}
|
||||
{#<span class="description-percentage text-yellow"><i class="fa fa-caret-left"></i> 0%</span>#}
|
||||
{#<span class="description-percentage text-green"><i class="fa fa-caret-up"></i> 20%</span>#}
|
||||
{#<span class="description-percentage text-red"><i class="fa fa-caret-down"></i> 18%</span>#}
|
||||
<h5 class="description-header">{{ data }}</h5>
|
||||
<span class="description-text">{{ widget.title|trans }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,18 +8,17 @@
|
||||
{% if projectStats|length > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-{{ admin_lte_context.widget.type }} WidgetUserTeamProjects" id="{{ widgetId }}">
|
||||
{% if not title is empty %}
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ title|trans }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="box-body">
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_attributes %}id="{{ widgetId }}"{% endblock %}
|
||||
{% block box_title %}
|
||||
{% if not title is empty %}{{ title|trans }}{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-hover dataTable" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.project'|trans }}</th>
|
||||
<th class="hidden-xs">{{ 'label.team'|trans }}</th>
|
||||
<th class="hidden-xs hidden-sm hidden-md">{{ 'label.team'|trans }}</th>
|
||||
<th style="width:50%">{{ 'label.progress'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -32,7 +31,7 @@
|
||||
<br>
|
||||
<small>{{ widgets.label_customer(stats.project.customer) }}</small>
|
||||
</td>
|
||||
<td class="hidden-xs">
|
||||
<td class="hidden-xs hidden-sm hidden-md">
|
||||
{% for team in project.teams %}
|
||||
{% if app.user.isInTeam(team) %}
|
||||
{{ widgets.label_team(team) }}
|
||||
@@ -51,10 +50,9 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,58 +1,21 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set teams = data %}
|
||||
{% set title = options.title|default('label.my_teams') %}
|
||||
{% set title = options.title|default(title|default('label.teams')) %}
|
||||
{% set widgetId = options.id %}
|
||||
|
||||
|
||||
{% if teams|length > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-{{ admin_lte_context.widget.type }} WidgetUserTeams" id="{{ widgetId }}">
|
||||
{% if not title is empty %}
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ title|trans }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="box-body">
|
||||
<table class="table table-hover dataTable" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.team'|trans }}</th>
|
||||
<th class="text-center">{{ 'label.teamlead'|trans }}</th>
|
||||
<th>{{ 'label.user'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ team.name }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ widgets.user_avatar(team.teamlead) }}
|
||||
</td>
|
||||
<td class="avatars">
|
||||
{% set userTeamCount = team.users|length %}
|
||||
{% for user in team.users %}
|
||||
{% set teamHiddenId = widgetId ~ '_' ~ team.id ~ '_hiddenUser' %}
|
||||
{{ widgets.user_avatar(user) }}
|
||||
{% if userTeamCount > 5 and loop.index == 5 and not loop.last %}
|
||||
<a href="#" onclick="$('#{{ teamHiddenId }}').toggleClass('hidden');$(this).hide();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - 5)}) }}</a>
|
||||
<span class="hidden" id="{{ teamHiddenId }}">
|
||||
{% endif %}
|
||||
{% if userTeamCount > 5 and loop.index != 5 and loop.last %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'border': false} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_title %}{{ title|trans }}{% endblock %}
|
||||
{% block box_attributes %}id="{{ widgetId }}"{% endblock %}
|
||||
{% block box_body_class %}no-padding{% endblock %}
|
||||
{% block box_body %}
|
||||
{{ widgets.team_list(teams, false) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -54,7 +54,6 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('Unknown role "%s"', $role));
|
||||
break;
|
||||
}
|
||||
|
||||
return $client;
|
||||
|
||||
@@ -47,7 +47,7 @@ class CreateUserCommandTest extends KernelTestCase
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$this->assertStringContainsString('[ERROR] plainPassword (foobar)', $output);
|
||||
$this->assertStringContainsString('The password is too short.', $output);
|
||||
$this->assertStringContainsString('This value is too short. It should have 8 characters or more.', $output);
|
||||
}
|
||||
|
||||
public function testCreateUser()
|
||||
|
||||
@@ -299,7 +299,7 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
protected function assertHasFlashSuccess(Client $client, string $message = null)
|
||||
{
|
||||
$node = $client->getCrawler()->filter('div.alert.alert-success.alert-dismissible');
|
||||
self::assertNotEmpty($node->text());
|
||||
self::assertGreaterThan(0, $node->count(), 'Could not find flash success message');
|
||||
if (null !== $message) {
|
||||
self::assertStringContainsString($message, $node->text());
|
||||
}
|
||||
@@ -312,7 +312,7 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
protected function assertHasFlashError(Client $client, string $message = null)
|
||||
{
|
||||
$node = $client->getCrawler()->filter('div.alert.alert-error.alert-dismissible');
|
||||
self::assertNotEmpty($node->text());
|
||||
self::assertGreaterThan(0, $node->count(), 'Could not find flash error message');
|
||||
if (null !== $message) {
|
||||
self::assertStringContainsString($message, $node->text());
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerMeta;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TeamFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
|
||||
@@ -69,21 +69,133 @@ class CustomerControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_customer_admin', 5);
|
||||
}
|
||||
|
||||
public function testBudgetAction()
|
||||
public function testDetailsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
|
||||
self::assertHasProgressbar($client);
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#customer_details_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#project_list_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#budget_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box a.btn-box-tool');
|
||||
self::assertEquals(2, $node->count());
|
||||
}
|
||||
|
||||
public function testAddCommentAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
|
||||
$form = $client->getCrawler()->filter('form[name=customer_comment_form]')->form();
|
||||
$client->submit($form, [
|
||||
'customer_comment_form' => [
|
||||
'message' => 'A beautiful and short comment **with some** markdown formatting',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('<p>A beautiful and short comment <strong>with some</strong> markdown formatting</p>', $node->html());
|
||||
}
|
||||
|
||||
public function testDeleteCommentAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
|
||||
$form = $client->getCrawler()->filter('form[name=customer_comment_form]')->form();
|
||||
$client->submit($form, [
|
||||
'customer_comment_form' => [
|
||||
'message' => 'Blah foo bar',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('Blah foo bar', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.confirmation-link');
|
||||
self::assertEquals($this->createUrl('/admin/customer/1/comment_delete'), $node->attr('href'));
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/customer/1/comment_delete');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('There were no comments posted yet', $node->html());
|
||||
}
|
||||
|
||||
public function testPinCommentAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
|
||||
$form = $client->getCrawler()->filter('form[name=customer_comment_form]')->form();
|
||||
$client->submit($form, [
|
||||
'customer_comment_form' => [
|
||||
'message' => 'Blah foo bar',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('Blah foo bar', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
self::assertEquals(0, $node->count());
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/customer/1/comment_pin');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
self::assertEquals(1, $node->count());
|
||||
self::assertEquals($this->createUrl('/admin/customer/1/comment_pin'), $node->attr('href'));
|
||||
}
|
||||
|
||||
public function testCreateDefaultTeamAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body');
|
||||
self::assertStringContainsString('Visible to everyone, as no team was assigned yet.', $node->text());
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/customer/1/create_team');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body');
|
||||
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body table tbody tr');
|
||||
self::assertEquals(1, $node->count());
|
||||
}
|
||||
|
||||
public function testProjectsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/projects/1');
|
||||
$node = $client->getCrawler()->filter('div.box#project_list_box .box-body table tbody tr');
|
||||
self::assertEquals(1, $node->count());
|
||||
|
||||
/** @var EntityManager $em */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setProjects($em->getRepository(Project::class)->findAll());
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_ADMIN));
|
||||
$customer = $em->getRepository(Customer::class)->find(1);
|
||||
$fixture = new ProjectFixtures();
|
||||
$fixture->setAmount(9); // to trigger a second page (every third activity is hidden)
|
||||
$fixture->setCustomers([$customer]);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/budget');
|
||||
self::assertHasProgressbar($client);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/1/projects/1');
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#project_list_box .box-tools ul.pagination li');
|
||||
self::assertEquals(4, $node->count());
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#project_list_box .box-body table tbody tr');
|
||||
self::assertEquals(5, $node->count());
|
||||
}
|
||||
|
||||
public function testCreateAction()
|
||||
@@ -107,9 +219,8 @@ class CustomerControllerTest extends ControllerBaseTest
|
||||
'name' => 'Test Customer',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/'));
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/2/details'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertHasFlashSuccess($client);
|
||||
}
|
||||
|
||||
@@ -137,9 +248,8 @@ class CustomerControllerTest extends ControllerBaseTest
|
||||
'name' => 'Test Customer 2'
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/'));
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->request($client, '/admin/customer/1/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=customer_edit_form]')->form();
|
||||
$this->assertEquals('Test Customer 2', $editForm->get('customer_edit_form[name]')->getValue());
|
||||
|
||||
@@ -38,9 +38,9 @@ class DashboardControllerTest extends ControllerBaseTest
|
||||
]);
|
||||
$this->request($client, '/dashboard/');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
self::assertEquals(1, $client->getCrawler()->filter('section.content .WidgetUserTeams')->count());
|
||||
self::assertEquals(1, $client->getCrawler()->filter('section.content #WidgetUserTeams')->count());
|
||||
// team 1 has no project assignment right now
|
||||
self::assertEquals(0, $client->getCrawler()->filter('section.content .WidgetUserTeamProjects')->count());
|
||||
self::assertEquals(0, $client->getCrawler()->filter('section.content #WidgetUserTeamProjects')->count());
|
||||
}
|
||||
|
||||
public function testIndexActionForAdmin()
|
||||
|
||||
@@ -29,7 +29,7 @@ class PermissionControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/permissions');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 83);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 101);
|
||||
$this->assertPageActions($client, [
|
||||
'back' => $this->createUrl('/admin/user/'),
|
||||
'roles modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||
|
||||
@@ -88,7 +88,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$content = $client->getResponse()->getContent();
|
||||
|
||||
$this->assertStringContainsString('<h3 class="box-title">About me</h3>', $content);
|
||||
$this->assertStringContainsString('<span class="pull-right badge bg-blue">' . $username . '</span>', $content);
|
||||
$this->assertStringContainsString('<td class="text-nowrap pull-right">' . $username . '</td>', $content);
|
||||
}
|
||||
|
||||
public function getTabTestData()
|
||||
@@ -133,8 +133,8 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/edit');
|
||||
|
||||
/** @var User $user */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var User $user */
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
|
||||
$this->assertEquals(UserFixtures::USERNAME_USER, $user->getUsername());
|
||||
@@ -209,8 +209,8 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/password');
|
||||
|
||||
/** @var User $user */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var User $user */
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
|
||||
/** @var EncoderFactoryInterface $passwordEncoder */
|
||||
@@ -248,8 +248,8 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/api-token');
|
||||
|
||||
/** @var User $user */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var User $user */
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
/** @var EncoderFactoryInterface $passwordEncoder */
|
||||
$passwordEncoder = $client->getContainer()->get('test.PasswordEncoder');
|
||||
@@ -293,8 +293,8 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/roles');
|
||||
|
||||
/** @var User $user */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var User $user */
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
|
||||
$this->assertEquals(['ROLE_USER'], $user->getRoles());
|
||||
@@ -385,8 +385,8 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->request($client, '/profile/' . $username . '/prefs');
|
||||
|
||||
/** @var User $user */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var User $user */
|
||||
$user = $this->getUserByName($em, $username);
|
||||
|
||||
$this->assertEquals($hourlyRateOriginal, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Project;
|
||||
use App\Entity\ProjectMeta;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\ActivityFixtures;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TeamFixtures;
|
||||
@@ -69,21 +70,150 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_project_admin', 5);
|
||||
}
|
||||
|
||||
public function testBudgetAction()
|
||||
public function testDetailsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
/** @var EntityManager $em */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$project = $em->getRepository(Project::class)->find(1);
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setProjects($em->getRepository(Project::class)->findAll());
|
||||
$fixture->setProjects([$project]);
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_ADMIN));
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$project = $em->getRepository(Project::class)->find(1);
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(6); // to trigger a second page
|
||||
$fixture->setProjects([$project]);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/budget');
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/details');
|
||||
self::assertHasProgressbar($client);
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#project_details_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#activity_list_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#budget_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box');
|
||||
self::assertEquals(1, $node->count());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box a.btn-box-tool');
|
||||
self::assertEquals(2, $node->count());
|
||||
}
|
||||
|
||||
public function testAddCommentAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/details');
|
||||
$form = $client->getCrawler()->filter('form[name=project_comment_form]')->form();
|
||||
$client->submit($form, [
|
||||
'project_comment_form' => [
|
||||
'message' => 'A beautiful and long comment **with some** markdown formatting',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('<p>A beautiful and long comment <strong>with some</strong> markdown formatting</p>', $node->html());
|
||||
}
|
||||
|
||||
public function testDeleteCommentAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/details');
|
||||
$form = $client->getCrawler()->filter('form[name=project_comment_form]')->form();
|
||||
$client->submit($form, [
|
||||
'project_comment_form' => [
|
||||
'message' => 'Foo bar blub',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('Foo bar blub', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.confirmation-link');
|
||||
self::assertEquals($this->createUrl('/admin/project/1/comment_delete'), $node->attr('href'));
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/project/1/comment_delete');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('There were no comments posted yet', $node->html());
|
||||
}
|
||||
|
||||
public function testPinCommentAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/details');
|
||||
$form = $client->getCrawler()->filter('form[name=project_comment_form]')->form();
|
||||
$client->submit($form, [
|
||||
'project_comment_form' => [
|
||||
'message' => 'Foo bar blub',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box div.box-comments');
|
||||
self::assertStringContainsString('Foo bar blub', $node->html());
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
self::assertEquals(0, $node->count());
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/project/1/comment_pin');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#comments_box .box-comment a.btn.active');
|
||||
self::assertEquals(1, $node->count());
|
||||
self::assertEquals($this->createUrl('/admin/project/1/comment_pin'), $node->attr('href'));
|
||||
}
|
||||
|
||||
public function testCreateDefaultTeamAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/details');
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body');
|
||||
self::assertStringContainsString('Visible to everyone, as no team was assigned yet.', $node->text());
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/admin/project/1/create_team');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body');
|
||||
self::assertStringContainsString('Only visible to the following teams and all admins.', $node->text());
|
||||
$node = $client->getCrawler()->filter('div.box#team_listing_box .box-body table tbody tr');
|
||||
self::assertEquals(1, $node->count());
|
||||
}
|
||||
|
||||
public function testActivitiesAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/activities/1');
|
||||
self::assertEquals('', $client->getResponse()->getContent());
|
||||
|
||||
/** @var EntityManager $em */
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$project = $em->getRepository(Project::class)->find(1);
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(9); // to trigger a second page (every third activity is hidden)
|
||||
$fixture->setProjects([$project]);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/1/activities/1');
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#activity_list_box .box-tools ul.pagination li');
|
||||
self::assertEquals(4, $node->count());
|
||||
|
||||
$node = $client->getCrawler()->filter('div.box#activity_list_box .box-body table tbody tr');
|
||||
self::assertEquals(5, $node->count());
|
||||
}
|
||||
|
||||
public function testCreateAction()
|
||||
@@ -98,9 +228,8 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
'name' => 'Test 2',
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/'));
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/2/details'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertHasFlashSuccess($client);
|
||||
}
|
||||
|
||||
@@ -160,9 +289,8 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$client->submit($form, [
|
||||
'project_edit_form' => ['name' => 'Test 2']
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/'));
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
|
||||
$client->followRedirect();
|
||||
$this->assertHasDataTable($client);
|
||||
$this->request($client, '/admin/project/1/edit');
|
||||
$editForm = $client->getCrawler()->filter('form[name=project_edit_form]')->form();
|
||||
$this->assertEquals('Test 2', $editForm->get('project_edit_form[name]')->getValue());
|
||||
|
||||
@@ -219,7 +219,7 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
$client->submit($form, [
|
||||
'system_configuration_form_theme' => [
|
||||
'configuration' => [
|
||||
['name' => 'theme.select_type', 'value' => 'selectpicker'],
|
||||
['name' => 'theme.autocomplete_chars', 'value' => 5],
|
||||
['name' => 'timesheet.markdown_content', 'value' => 1],
|
||||
]
|
||||
]
|
||||
|
||||
@@ -123,6 +123,29 @@ class TeamControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals('Test Team 2', $editForm->get('team_edit_form[name]')->getValue());
|
||||
}
|
||||
|
||||
public function testEditMemberAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new TeamFixtures();
|
||||
$fixture->setAmount(2);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/admin/teams/1/edit_member');
|
||||
$form = $client->getCrawler()->filter('form[name=team_edit_form]')->form();
|
||||
$this->assertNotEmpty($form->get('team_edit_form[name]')->getValue());
|
||||
$client->submit($form, [
|
||||
'team_edit_form' => [
|
||||
'name' => 'Test Team 2'
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/teams/1/edit'));
|
||||
$client->followRedirect();
|
||||
$editForm = $client->getCrawler()->filter('form[name=team_edit_form]')->form();
|
||||
$this->assertEquals('Test Team 2', $editForm->get('team_edit_form[name]')->getValue());
|
||||
}
|
||||
|
||||
public function testEditCustomerAccessAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Tests\DataFixtures;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
@@ -36,6 +36,10 @@ final class ActivityFixtures extends Fixture
|
||||
* @var callable
|
||||
*/
|
||||
private $callback;
|
||||
/**
|
||||
* @var Project[]
|
||||
*/
|
||||
private $projects = [];
|
||||
|
||||
/**
|
||||
* Will be called prior to persisting the object.
|
||||
@@ -79,15 +83,28 @@ final class ActivityFixtures extends Fixture
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project[] $projects
|
||||
* @return ActivityFixtures
|
||||
*/
|
||||
public function setProjects(array $projects): ActivityFixtures
|
||||
{
|
||||
$this->projects = $projects;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$projects = $this->getAllProjects($manager);
|
||||
$projects = $this->projects;
|
||||
if (empty($projects)) {
|
||||
$projects = $this->getAllProjects($manager);
|
||||
}
|
||||
$faker = Factory::create();
|
||||
|
||||
// random amount of timesheet entries for every user
|
||||
for ($i = 0; $i < $this->amount; $i++) {
|
||||
$project = null;
|
||||
if (false === $this->isGlobal) {
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Tests\DataFixtures;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Tests\DataFixtures;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Tests\DataFixtures;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
@@ -32,6 +32,10 @@ final class ProjectFixtures extends Fixture
|
||||
* @var callable
|
||||
*/
|
||||
private $callback;
|
||||
/**
|
||||
* @var Customer[]
|
||||
*/
|
||||
private $customers = [];
|
||||
|
||||
public function getAmount(): int
|
||||
{
|
||||
@@ -52,6 +56,17 @@ final class ProjectFixtures extends Fixture
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer[] $customers
|
||||
* @return ProjectFixtures
|
||||
*/
|
||||
public function setCustomers(array $customers): ProjectFixtures
|
||||
{
|
||||
$this->customers = $customers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called prior to persisting the object.
|
||||
*
|
||||
@@ -70,7 +85,10 @@ final class ProjectFixtures extends Fixture
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$customers = $this->getAllCustomers($manager);
|
||||
$customers = $this->customers;
|
||||
if (empty($customers)) {
|
||||
$customers = $this->getAllCustomers($manager);
|
||||
}
|
||||
$faker = Factory::create();
|
||||
|
||||
for ($i = 0; $i < $this->amount; $i++) {
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Tests\DataFixtures;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Entity\Customer;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
|
||||
50
tests/Entity/AbstractCommentEntityTest.php
Normal file
50
tests/Entity/AbstractCommentEntityTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\CommentInterface;
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class AbstractCommentEntityTest extends TestCase
|
||||
{
|
||||
abstract protected function getEntity(): CommentInterface;
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = $this->getEntity();
|
||||
|
||||
self::assertNull($sut->getId());
|
||||
self::assertNull($sut->getMessage());
|
||||
self::assertNull($sut->getCreatedBy());
|
||||
self::assertNotNull($sut->getCreatedAt());
|
||||
self::assertInstanceOf(\DateTime::class, $sut->getCreatedAt());
|
||||
self::assertFalse($sut->isPinned());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = $this->getEntity();
|
||||
|
||||
$sut->setPinned(true);
|
||||
self::assertTrue($sut->isPinned());
|
||||
|
||||
$user = new User();
|
||||
$sut->setCreatedBy($user);
|
||||
self::assertSame($user, $sut->getCreatedBy());
|
||||
|
||||
$date = new \DateTime();
|
||||
$sut->setCreatedAt($date);
|
||||
self::assertSame($date, $sut->getCreatedAt());
|
||||
|
||||
$sut->setMessage('slödkfjaölsdkjflaksjdfölaksjdfölakjsdöfl');
|
||||
self::assertEquals('slödkfjaölsdkjflaksjdfölaksjdfölakjsdöfl', $sut->getMessage());
|
||||
}
|
||||
}
|
||||
36
tests/Entity/CustomerCommentTest.php
Normal file
36
tests/Entity/CustomerCommentTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Entity\CommentInterface;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\CustomerComment
|
||||
* @covers \App\Entity\CommentTableTypeTrait
|
||||
*/
|
||||
class CustomerCommentTest extends AbstractCommentEntityTest
|
||||
{
|
||||
protected function getEntity(): CommentInterface
|
||||
{
|
||||
return new CustomerComment();
|
||||
}
|
||||
|
||||
public function testEntitySpecificMethods()
|
||||
{
|
||||
$sut = new CustomerComment();
|
||||
self::assertNull($sut->getCustomer());
|
||||
|
||||
$customer = new Customer();
|
||||
self::assertInstanceOf(CustomerComment::class, $sut->setCustomer($customer));
|
||||
self::assertSame($customer, $sut->getCustomer());
|
||||
}
|
||||
}
|
||||
36
tests/Entity/ProjectCommentTest.php
Normal file
36
tests/Entity/ProjectCommentTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Entity\CommentInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectComment;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\ProjectComment
|
||||
* @covers \App\Entity\CommentTableTypeTrait
|
||||
*/
|
||||
class ProjectCommentTest extends AbstractCommentEntityTest
|
||||
{
|
||||
protected function getEntity(): CommentInterface
|
||||
{
|
||||
return new ProjectComment();
|
||||
}
|
||||
|
||||
public function testEntitySpecificMethods()
|
||||
{
|
||||
$sut = new ProjectComment();
|
||||
self::assertNull($sut->getProject());
|
||||
|
||||
$project = new Project();
|
||||
self::assertInstanceOf(ProjectComment::class, $sut->setProject($project));
|
||||
self::assertSame($project, $sut->getProject());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user