Compare commits

...

27 Commits
0.3 ... 0.4

Author SHA1 Message Date
Kevin Papst
6fb6a16246 prepare release 0.4 (#292) 2018-09-01 21:05:29 +02:00
Kevin Papst
3af8070a57 fixed money display in dashboard widgets (#291) 2018-09-01 19:47:03 +02:00
Kevin Papst
ebe007eb78 added configurable formats for duration (#287) 2018-09-01 17:54:17 +02:00
Kevin Papst
55b7e192aa Symfony update 4.1.4 (#290) 2018-09-01 14:23:38 +02:00
Kevin Papst
62f91f4d38 do not allow to stop already stopped timesheets (#289) 2018-09-01 13:00:30 +02:00
Kevin Papst
b1546c0568 Updated russian translations (#286) 2018-09-01 11:20:54 +02:00
Thierry Pasquier
af2d1c3a6e fix french date format and update translations (#279) 2018-08-23 20:46:30 +02:00
Thierry Pasquier
ca893b303a Fix standard french date format 2018-08-19 10:15:24 +02:00
Kevin Papst
142369d651 activated spanish, french and italian 2018-08-18 23:39:40 +02:00
Thierry Pasquier
3241c73e7c Added French translations (#275) 2018-08-18 23:39:40 +02:00
Ivan Castro
d6d9abd05e added Spanish translations (#266)
Spanish translation for kimai
2018-08-18 23:39:40 +02:00
Kevin Papst
85ff2ff9de updated to AdminLTE 2.4.8 (#274) 2018-08-17 23:54:26 +02:00
Kevin Papst
dbbb434723 dashboard widgets are configurable via config (#269) 2018-08-17 23:17:02 +02:00
Ingo Ebel
7804ef83ce fixing migration_v1.md url (#270) 2018-08-13 10:15:34 +02:00
Kevin Papst
fb2d898e4c fixed calendar for empty timesheet descriptions (#265) 2018-08-10 08:19:30 +02:00
Kevin Papst
c6e99b35d5 moved twig globals to dynamic compiler pass (#264) 2018-08-10 00:11:48 +02:00
Kevin Papst
3dcd38eca7 Added config setting to disable password-reset and user-registration (#261) 2018-08-09 20:44:49 +02:00
Kevin Papst
39ea44fdc3 fixed .htaccess for apache users (#260) 2018-08-08 23:35:02 +02:00
Kevin Papst
2f84f3751a Added basic API endpoints (#258) 2018-08-08 23:10:45 +02:00
Kevin Papst
a7780dac9e added italian translation (#253) 2018-08-08 00:00:24 +02:00
Kevin Papst
e549a0824f Hotfix for Symfony Flex incompatibility with latest composer changes (#257) 2018-08-07 20:17:56 +02:00
Kevin Papst
8b8583d48b Merge branch 'translations' into master 2018-07-31 17:37:59 +02:00
Kevin Papst
a8a8424ced Added delete user feature #225 (#249) 2018-07-31 13:16:40 +02:00
Kevin Papst
0996eca3c5 dashboard widgets from event (#246) 2018-07-28 23:18:28 +02:00
Kevin Papst
e6f8bc8ae2 Added timesheet-calendar view (#236) 2018-07-27 20:19:56 +02:00
Kevin Papst
5ccc1c991b Improved data fixtures with Faker (#234) 2018-07-24 19:24:58 +02:00
Simon Schaufelberger
1dd5f2b72f Translations 2018-06-23 00:48:11 +02:00
196 changed files with 13350 additions and 2383 deletions

View File

@@ -1,2 +1,2 @@
unreleased=true
future-release=0.3
future-release=0.4

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@
###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.result.cache
###< phpunit/phpunit ###
###> friendsofphp/php-cs-fixer ###

View File

@@ -1,21 +1,53 @@
# Change Log
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4) (2018-09-01)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
**Fixed bugs:**
- Don't allow to stop an already stopped entry [\#282](https://github.com/kevinpapst/kimai2/issues/282)
- installed it, and then what??? [\#255](https://github.com/kevinpapst/kimai2/issues/255)
- fixed calendar for empty timesheet descriptions [\#265](https://github.com/kevinpapst/kimai2/pull/265) ([kevinpapst](https://github.com/kevinpapst))
- fixed .htaccess for apache users [\#260](https://github.com/kevinpapst/kimai2/pull/260) ([kevinpapst](https://github.com/kevinpapst))
**Closed issues:**
- Improve dashboard display [\#280](https://github.com/kevinpapst/kimai2/issues/280)
- CSV export [\#263](https://github.com/kevinpapst/kimai2/issues/263)
- Spanish Translation Contrib [\#248](https://github.com/kevinpapst/kimai2/issues/248)
- Administration of projects throws error if locale is "de" [\#247](https://github.com/kevinpapst/kimai2/issues/247)
- Web Installer Package for near-automated installs from browser [\#235](https://github.com/kevinpapst/kimai2/issues/235)
- Delete user [\#225](https://github.com/kevinpapst/kimai2/issues/225)
- Feature request - Monthly overview / report [\#207](https://github.com/kevinpapst/kimai2/issues/207)
**Merged pull requests:**
- fixed money display in dashboard widgets [\#291](https://github.com/kevinpapst/kimai2/pull/291) ([kevinpapst](https://github.com/kevinpapst))
- Symfony update 4.1.4 [\#290](https://github.com/kevinpapst/kimai2/pull/290) ([kevinpapst](https://github.com/kevinpapst))
- do not allow to stop already stopped timesheets [\#289](https://github.com/kevinpapst/kimai2/pull/289) ([kevinpapst](https://github.com/kevinpapst))
- added configurable formats for duration [\#287](https://github.com/kevinpapst/kimai2/pull/287) ([kevinpapst](https://github.com/kevinpapst))
- Updated russian translations [\#286](https://github.com/kevinpapst/kimai2/pull/286) ([kevinpapst](https://github.com/kevinpapst))
- Update french translation [\#279](https://github.com/kevinpapst/kimai2/pull/279) ([jeau](https://github.com/jeau))
- Fix standard date format in french [\#278](https://github.com/kevinpapst/kimai2/pull/278) ([jeau](https://github.com/jeau))
- new translations: spanish, french and italian [\#276](https://github.com/kevinpapst/kimai2/pull/276) ([kevinpapst](https://github.com/kevinpapst))
- French translation for Kimai [\#275](https://github.com/kevinpapst/kimai2/pull/275) ([jeau](https://github.com/jeau))
- updated to AdminLTE 2.4.8 [\#274](https://github.com/kevinpapst/kimai2/pull/274) ([kevinpapst](https://github.com/kevinpapst))
- fixing migration\_v1.md url [\#270](https://github.com/kevinpapst/kimai2/pull/270) ([ltsavar](https://github.com/ltsavar))
- dashboard widgets are configurable via config [\#269](https://github.com/kevinpapst/kimai2/pull/269) ([kevinpapst](https://github.com/kevinpapst))
- Spanish translation for Kimai [\#266](https://github.com/kevinpapst/kimai2/pull/266) ([cobaltos](https://github.com/cobaltos))
- moved theme settings to dynamic compiler pass [\#264](https://github.com/kevinpapst/kimai2/pull/264) ([kevinpapst](https://github.com/kevinpapst))
- Added config setting to disable password-reset and user-registration [\#261](https://github.com/kevinpapst/kimai2/pull/261) ([kevinpapst](https://github.com/kevinpapst))
- Added basic API endpoints [\#258](https://github.com/kevinpapst/kimai2/pull/258) ([kevinpapst](https://github.com/kevinpapst))
- Hotfix for Symfony Flex incompatibility with latest composer changes [\#257](https://github.com/kevinpapst/kimai2/pull/257) ([kevinpapst](https://github.com/kevinpapst))
- added italian translation [\#253](https://github.com/kevinpapst/kimai2/pull/253) ([kevinpapst](https://github.com/kevinpapst))
- Delete user [\#249](https://github.com/kevinpapst/kimai2/pull/249) ([kevinpapst](https://github.com/kevinpapst))
- dashboard widgets from event [\#246](https://github.com/kevinpapst/kimai2/pull/246) ([kevinpapst](https://github.com/kevinpapst))
- Added timesheet-calendar view [\#236](https://github.com/kevinpapst/kimai2/pull/236) ([kevinpapst](https://github.com/kevinpapst))
- Improved data fixtures [\#234](https://github.com/kevinpapst/kimai2/pull/234) ([kevinpapst](https://github.com/kevinpapst))
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3) (2018-07-22)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.2...0.3)
**Update from 0.2:**
You need to adjust your `.env` file and add your `from` address for [all emails](https://github.com/kevinpapst/kimai2/blob/master/var/docs/emails.md) generated by Kimai 2:
```
MAILER_FROM=kimai@example.com
```
Users upgrading from 0.2 need to execute:
```
bin/console doctrine:migrations:version --add 20180701120000
bin/console doctrine:migrations:migrate
```
**Fixed bugs:**
- Missing assets in fresh installation [\#213](https://github.com/kevinpapst/kimai2/issues/213)
@@ -25,14 +57,14 @@ bin/console doctrine:migrations:migrate
**Closed issues:**
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
- Installation Issue of version1.3.1 [\#208](https://github.com/kevinpapst/kimai2/issues/208)
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
- Add favicon [\#205](https://github.com/kevinpapst/kimai2/issues/205)
- Install Kimai as WebApp [\#203](https://github.com/kevinpapst/kimai2/issues/203)
- Replace AvanzuAdminTheme with AdminLTE bundle [\#201](https://github.com/kevinpapst/kimai2/issues/201)
- Document DB requirements in install docu [\#196](https://github.com/kevinpapst/kimai2/issues/196)
- easier creation of tasks [\#195](https://github.com/kevinpapst/kimai2/issues/195)
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
- Integrate lock bot [\#183](https://github.com/kevinpapst/kimai2/issues/183)
- Upgrade to FontAwesome 5 [\#179](https://github.com/kevinpapst/kimai2/issues/179)
- Add dynamic column filter [\#174](https://github.com/kevinpapst/kimai2/issues/174)
@@ -42,6 +74,7 @@ bin/console doctrine:migrations:migrate
**Merged pull requests:**
- prepare release 0.3 [\#231](https://github.com/kevinpapst/kimai2/pull/231) ([kevinpapst](https://github.com/kevinpapst))
- Easier creation of Tasks/Projects/User [\#230](https://github.com/kevinpapst/kimai2/pull/230) ([kevinpapst](https://github.com/kevinpapst))
- Added email configuration docs [\#228](https://github.com/kevinpapst/kimai2/pull/228) ([kevinpapst](https://github.com/kevinpapst))
- Show revenue only for ROLE\_ADMIN [\#227](https://github.com/kevinpapst/kimai2/pull/227) ([kevinpapst](https://github.com/kevinpapst))
@@ -114,6 +147,7 @@ bin/console doctrine:migrations:migrate
**Merged pull requests:**
- \[WIP\] Translations [\#173](https://github.com/kevinpapst/kimai2/pull/173) ([simonschaufi](https://github.com/simonschaufi))
- prepare v0.2 release \#177 [\#178](https://github.com/kevinpapst/kimai2/pull/178) ([kevinpapst](https://github.com/kevinpapst))
- various documentation updates [\#176](https://github.com/kevinpapst/kimai2/pull/176) ([kevinpapst](https://github.com/kevinpapst))
- code styles: header\_comment and ordered\_imports [\#175](https://github.com/kevinpapst/kimai2/pull/175) ([kevinpapst](https://github.com/kevinpapst))

View File

@@ -18,7 +18,10 @@ Right now its in an early development phase, its usable but some advanced featur
But we already support to [import your timesheets](migration_v1.md) from Kimai v1.
It is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json)...
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json).
Kimai is a multi-language application and already translated to English. German, Italian, Frensh, Spanish and Russian.
If you want to support us in translating Kimai, please [read this documentation](var/docs/translations.md).
### Requirements
@@ -28,21 +31,21 @@ It is developed with modern frameworks like [Symfony v4](https://github.com/symf
- The [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
- If you use MariaDB, make sure its at least v10.7.2 (see [FAQ](var/docs/faq.md))
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
- A modern browser, Kimai v2 might be broken on old browsers like IE 9
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
## Documentation & Roadmap
Looking for more information about using Kimai? Check out our more detailed [documentation](var/docs/).
You can see our development roadmap for the future in the [Milestones](milestones/) sections,
current work is organized in the [Project](projects/) planning boards.
You can see our development roadmap for the future in the [Milestones](milestones/) sections.
Our roadmap is open for changes and input from the community, please [sent us](issues/) your ideas and questions.
## Installation
There are [further infos about installation](var/docs/installation.md) if you have to use FTP or want to develop with Kimai.
If you want to install Kimai v2 in your production environment, then SSH into your server and change to your webserevr root.
If you want to install Kimai 2 in your production environment, then SSH into your server and change to your webserver root.
You need to install Git and [Composer](https://getcomposer.org/doc/00-intro.md) if you haven't already.
First clone this repo:
@@ -96,7 +99,7 @@ Create your first user with the following command. You will be asked to enter a
```bash
bin/console kimai:create-user username admin@example.com ROLE_SUPER_ADMIN
```
_Tip: You can skip the "create user" step, if you are going to [import data from Kimai v1](migration_v1.md)._
_Tip: You can skip the "create user" step, if you are going to [import data from Kimai v1](var/docs/migration_v1.md)._
For available roles, please refer to the [user documentation](var/docs/users.md).

View File

@@ -1,5 +1,50 @@
# Upgrading Kimai 2
Database upgrades are currently ONLY provided for MySQL/MariaDB and SQLite.
If you plan on using e.g. PostgreSQL, please read more about the `bin/console doctrine:migrations:diff` and
`bin/console doctrine:migrations:migrate` commands and contact us, so we can integrate them into the official releases.
A normal upgrade can be executed with these commands:
```bash
git pull origin master
sudo -u www-data composer install --no-dev --optimize-autoloader
sudo -u www-data bin/console cache:clear --env=prod
sudo -u www-data bin/console cache:warmup --env=prod
bin/console doctrine:migrations:migrate
```
There might be version specific tasks that need to be executed before or after these steps, please see below
if your updated version is mentioned below.
## [0.4](https://github.com/kevinpapst/kimai2/releases/tag/0.4) (2018-09-01)
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
```
[ErrorException]
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
```
This can be fixed by updating Composer and Flex before executing the Kimai update:
```
sudo composer self-update
sudo -u www-data composer update symfony/flex --no-plugins
```
Then the full update can be executed as usual:
```bash
git pull origin master
sudo -u www-data composer install --no-dev --optimize-autoloader
sudo -u www-data bin/console cache:clear --env=prod
sudo -u www-data bin/console cache:warmup --env=prod
bin/console doctrine:migrations:migrate
```
## [0.3](https://github.com/kevinpapst/kimai2/releases/tag/0.3) (2018-07-22)
**Update from 0.2:**

View File

@@ -11,98 +11,13 @@
margin-bottom: 50px;
}
/* ================================ CONTENT ================================ */
.content {
padding: 15px 0;
}
.content-header {
h1 {
small {
display: none;
}
}
@media print{
.sf-toolbar, .control-sidebar {display: none !important;}
}
@media (min-width: $screen-sm-min) {
.content-header {
h1 {
small {
display: inline-block;
}
}
}
.content {
padding: 15px;
}
}
@import 'content';
@import 'toolbar';
/* ================================ NAVBAR ================================ */
.navbar-nav > li > a.ddt-large {
padding-top: 12px;
padding-bottom: 9px;
}
li.messages-menu ul.menu li .pull-left i {
color: #dd4b39;
}
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
overflow: hidden;
text-overflow: ellipsis;
}
.navbar-nav .start_record {
text-align: center;
}
/*
.ticktac:hover i.running{
color: #4ff131;
}
li.open .ticktac i.running{
color: #4ff131;
}
.ticktac i.stopped{
color: #fff;
}
.ticktac:hover i.stopped{
color: #4ff131;
}
.user-panel>.info {
left: 40px;
padding-top: 0px;
}
*/
/* ================================ TOOLBAR ================================ */
/* Page based action buttons in the upper right corner of the content area */
.content-header > .breadcrumb {
position: absolute;
float: right;
background: transparent;
}
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
.control-sidebar {
select {
color: $input-color;
background-color: $input-bg;
}
div.image {
img.img-circle {
max-width: 34px;
}
}
}
/* The filter form available on most pages above the datatable */
.toolbar form {
font-size: $font-size-base;
}
/* ================================ SIDEBAR ================================ */
/* Used for the Sidebar UserPanel (which is deactivated right now) */
@@ -117,40 +32,8 @@ footer.main-footer {
font-size: 80%;
}
/* ================================ PRINT ================================ */
@media print{
.sf-toolbar, .control-sidebar {display: none !important;}
}
/* ================================ INVOICES ================================ */
.invoice {
margin: 0;
}
@media (min-width: $screen-sm-min) {
.invoice {
margin: 10px 25px;
}
}
.page-header-small{
font-size: 14px;
}
div.invoice-address {
margin-top: 30px;
margin-bottom: 30px;
}
table.invoice-records {}
table.invoice-meta th {
padding-right: 40px;
}
table.invoice-sum th {
width: 70%;
}
table.invoice-sum th, table.invoice-sum td {
text-align: right;
}
@import 'invoice';
@import 'datatable';
@import 'calendar';
@import 'dashboard';
@import 'navbar';

7
assets/css/calendar.scss Normal file
View File

@@ -0,0 +1,7 @@
.popover-content {
.calendar-entry {
ul {
padding: 0 0 0 10px;
}
}
}

30
assets/css/content.scss Normal file
View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
.content {
padding: 15px 0;
}
.content-header {
h1 {
small {
display: none;
}
}
}
@media (min-width: $screen-sm-min) {
.content-header {
h1 {
small {
display: inline-block;
}
}
}
.content {
padding: 15px;
}
}

16
assets/css/dashboard.scss Normal file
View File

@@ -0,0 +1,16 @@
/*
* 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.
*/
section {
&.dashboard {
.info-box {
.info-box-text {
white-space: normal;
}
}
}
}

36
assets/css/invoice.scss Normal file
View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
.invoice {
margin: 0;
}
@media (min-width: $screen-sm-min) {
.invoice {
margin: 10px 25px;
}
}
.page-header-small{
font-size: 14px;
}
div.invoice-address {
margin-top: 30px;
margin-bottom: 30px;
}
table.invoice-records {}
table.invoice-meta th {
padding-right: 40px;
}
table.invoice-sum th {
width: 70%;
}
table.invoice-sum th, table.invoice-sum td {
text-align: right;
}

46
assets/css/navbar.scss Normal file
View File

@@ -0,0 +1,46 @@
/*
* 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.
*/
.navbar-nav > li > a.ddt-large {
padding-top: 12px;
padding-bottom: 9px;
}
li.messages-menu ul.menu li .pull-left i {
color: #dd4b39;
}
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
overflow: hidden;
text-overflow: ellipsis;
}
.navbar-nav .start_record {
text-align: center;
}
/*
.ticktac:hover i.running{
color: #4ff131;
}
li.open .ticktac i.running{
color: #4ff131;
}
.ticktac i.stopped{
color: #fff;
}
.ticktac:hover i.stopped{
color: #4ff131;
}
.user-panel>.info {
left: 40px;
padding-top: 0px;
}
*/

29
assets/css/toolbar.scss Normal file
View File

@@ -0,0 +1,29 @@
/*
* 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.
*/
/* Page based action buttons in the upper right corner of the content area */
.content-header > .breadcrumb {
position: absolute;
float: right;
background: transparent;
}
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
.control-sidebar {
select {
color: $input-color;
background-color: $input-bg;
}
div.image {
img.img-circle {
max-width: 34px;
}
}
}
/* The filter form available on most pages above the datatable */
.toolbar form {
font-size: $font-size-base;
}

View File

@@ -2,6 +2,15 @@ require('../../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
require('../css/app.scss');
require('../css/datatable.scss');
require('../css/calendar.scss');
require('fullcalendar');
require('fullcalendar/dist/gcal.min');
require('fullcalendar/dist/locale-all');
require('fullcalendar/dist/fullcalendar.min.css');
// ------ for charts ------
require('chart.js/dist/Chart.min');
// ------ Kimai itself ------
require('./kimai.js');

View File

@@ -10,10 +10,13 @@
"ext-pdo_sqlite": "*",
"beberlei/DoctrineExtensions": "^1.0",
"erusev/parsedown": "^1.6",
"friendsofsymfony/rest-bundle": "^2.3",
"friendsofsymfony/user-bundle": "~2.0",
"kevinpapst/adminlte-bundle": "^1.2",
"jms/serializer-bundle": "^2.4",
"kevinpapst/adminlte-bundle": "^2.0",
"nelmio/api-doc-bundle": "^3.2",
"ocramius/proxy-manager": "2.1.1",
"sensio/framework-extra-bundle": "^5.1",
"sensio/framework-extra-bundle": "^5.2",
"symfony/asset": "^4.0",
"symfony/console": "^4.0",
"symfony/dotenv": "^4.0",
@@ -27,6 +30,7 @@
"symfony/profiler-pack": "^1.0",
"symfony/security-bundle": "^4.0",
"symfony/security-csrf": "^4.0",
"symfony/serializer": "^4.0",
"symfony/swiftmailer-bundle": "^3.1",
"symfony/translation": "^4.0",
"symfony/twig-bundle": "^4.0",
@@ -40,6 +44,7 @@
"dama/doctrine-test-bundle": "^5.0",
"doctrine/doctrine-fixtures-bundle": "^3.0",
"friendsofphp/php-cs-fixer": "^2.10",
"fzaninotto/faker": "^1.8",
"phpunit/phpunit": "^7.0",
"squizlabs/php_codesniffer": "^3.2",
"symfony/browser-kit": "^4.0",
@@ -90,13 +95,7 @@
"extra": {
"symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
"allow-contrib": false
"allow-contrib": true
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/kevinpapst/AdminThemeBundle"
}
]
}
}

2454
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,4 +19,7 @@ return [
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
FOS\UserBundle\FOSUserBundle::class => ['all' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
];

View File

@@ -0,0 +1,7 @@
jms_serializer:
visitors:
json:
options:
- JSON_PRETTY_PRINT
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION

View File

@@ -0,0 +1,107 @@
fos_rest:
# disable_csrf_role: ROLE_API
# access_denied_listener:
# enabled: false
# service: null
# formats:
# name: ~
# unauthorized_challenge: null
# param_fetcher_listener:
# enabled: false
# force: false
# service: null
# cache_dir: '%kernel.cache_dir%/fos_rest'
# allowed_methods_listener:
# enabled: false
# service: null
routing_loader:
default_format: json
include_format: true
# prefix_methods: true
# body_converter:
# enabled: false
# validate: false
# validation_errors_argument: validationErrors
# service:
# router: router
# templating: templating
# serializer: null
# view_handler: fos_rest.view_handler.default
# inflector: fos_rest.inflector.doctrine
# validator: validator
# serializer:
# version: null
# groups: []
# serialize_null: false
view:
default_engine: twig
# force_redirects:
# name: ~
# mime_types:
# enabled: false
# service: null
# formats:
# name: ~
# formats:
# name: ~
# templating_formats:
# name: ~
# view_response_listener:
# enabled: true
# force: true
# service: null
failed_validation: 400
# empty_content: 204
# serialize_null: false
# jsonp_handler:
# callback_param: callback
# mime_type: application/javascript+jsonp
exception:
enabled: true
exception_controller: 'fos_rest.exception.controller:showAction'
# service: null
codes:
# 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
# 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
# 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 400
messages:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': true
# body_listener:
# enabled: true
# service: null
# default_format: null
# throw_exception_on_unsupported_content_type: false
# decoders:
# name: ~
# array_normalizer:
# service: null
# forms: false
format_listener:
enabled: true
# service: null
rules:
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, xml ] }
# host: null # URL host name
# methods: null # Method for URL
# stop: false
# attributes: []
# versioning:
# enabled: false
# default_version: ~
# resolvers:
# query:
# enabled: true
# parameter_name: version
# custom_header:
# enabled: true
# header_name: X-Accept-Version
# media_type:
# enabled: true
# regex: /(v|version)=(?P<version>[0-9\.]+)/
# guessing_order:
# - query
# - custom_header
# - media_type
zone:
- { path: ^/api/* }

View File

@@ -0,0 +1,12 @@
jms_serializer:
visitors:
xml:
format_output: '%kernel.debug%'
metadata:
directories:
FOSUB:
namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle"
App:
namespace_prefix: "App"
path: "%kernel.root_dir%/../config/serializer/App"

View File

@@ -1,5 +1,10 @@
kimai:
# Settings for the user management and login forms
user:
registration: true
password_reset: true
# All configs related to timesheet and record management
timesheet:
@@ -39,14 +44,110 @@ kimai:
# Language specific settings, like the date formats
languages:
de:
date_short: "d.m.Y"
date_short: 'd.m.Y'
duration: '%%h:%%m Stunden'
duration_short: '%%h:%%m h'
en:
date_short: "Y-m-d"
date_short: 'Y-m-d'
duration: '%%h:%%m hours'
duration_short: '%%h:%%m h'
it:
date_short: 'd.m.Y'
duration: '%%h:%%m h'
duration_short: '%%h:%%m h'
fr:
date_short: 'd/m/Y'
duration: '%%h h %%m min'
duration_short: '%%h:%%m h'
es:
date_short: 'd.m.Y'
duration: '%%h:%%m h'
duration_short: '%%h:%%m h'
ru:
date_short: "d.m.Y"
date_short: 'd.m.Y'
duration: '%%h:%%m Часов'
duration_short: '%%h:%%m h'
twig:
globals:
kimai_context:
box_color: "green" # a color for ???
active_warning: 3 # display a warning color if the user has at least X active recordings
# timesheet calendar configuration
calendar:
week_numbers: true
day_limit: 4
businessHours:
days: [1, 2, 3, 4, 5]
begin: '08:00'
end: '20:00'
# You can configure unlimited google calendars to display events for your company (e.g. holidays)
# Please read var/docs/configurations.md to find out more about calendar integration
# google:
# api_key: 'your-restricted-google-api-key'
# sources:
# holidays:
# id: 'de.german#holiday@group.v.calendar.google.com'
# color: '#ccc'
# theme related settings, will be available as twig globals at "kimai_context.*"
# please see documentation at var/docs/theme.md
theme:
# display a warning color if the user has at least X active recordings
active_warning: 3
# fallback color for all widgets that don't have a dedicated color
# possible options: blue, black, purple, yellow, red, green
box_color: 'green'
# Dashboard widget sections, please see documentation at var/docs/dashboard.md
dashboard:
user_duration:
title: dashboard.you
order: 10
permission: ROLE_USER
widgets: [userDurationToday, userDurationWeek, userDurationMonth, userDurationYear]
user_rates:
title: ~
order: 20
permission: ROLE_USER
widgets: [userAmountToday, userAmountWeek, userAmountMonth, userAmountYear]
duration:
title: dashboard.all
order: 30
permission: ROLE_TEAMLEAD
widgets: [durationToday, durationWeek, durationMonth, durationYear]
active_users:
title: ~
order: 40
permission: ROLE_TEAMLEAD
widgets: [activeUsersToday, activeUsersWeek, activeUsersMonth, activeUsersYear]
rates:
title: ~
order: 50
permission: ROLE_ADMIN
widgets: [amountToday, amountWeek, amountMonth, amountYear]
# All available widgets, please see documentation at var/docs/dashboard.md
widgets:
userDurationToday: { title: stats.durationToday, query: duration, user: true, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
userDurationWeek: { title: stats.durationWeek, query: duration, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
userDurationMonth: { title: stats.durationMonth, query: duration, user: true, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: duration, color: purple }
userDurationYear: { title: stats.durationYear, query: duration, user: true, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: duration, color: yellow }
userDurationTotal: { title: stats.durationTotal, query: duration, user: true, icon: duration, color: red }
userAmountToday: { title: stats.amountToday, query: rate, user: true, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
userAmountWeek: { title: stats.amountWeek, query: rate, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
userAmountMonth: { title: stats.amountMonth, query: rate, user: true, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: money, color: purple }
userAmountYear: { title: stats.amountYear, query: rate, user: true, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: money, color: yellow }
userAmountTotal: { title: stats.amountTotal, query: rate, user: true, icon: money, color: red }
durationToday: { title: stats.durationToday, query: duration, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
durationWeek: { title: stats.durationWeek, query: duration, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
durationMonth: { title: stats.durationMonth, query: duration, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: duration, color: purple }
durationYear: { title: stats.durationYear, query: duration, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: duration, color: yellow }
durationTotal: { title: stats.durationTotal, query: duration, icon: duration, color: red }
amountToday: { title: stats.amountToday, query: rate, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
amountWeek: { title: stats.amountWeek, query: rate, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
amountMonth: { title: stats.amountMonth, query: rate, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: money, color: purple }
amountYear: { title: stats.amountYear, query: rate, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: money, color: yellow }
amountTotal: { title: stats.amountTotal, query: rate, icon: money, color: red }
activeUsersToday: { title: stats.userActiveToday, query: users, begin: '00:00:00', end: '23:59:59', icon: user, color: green }
activeUsersWeek: { title: stats.userActiveWeek, query: users, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: user, color: blue }
activeUsersMonth: { title: stats.userActiveMonth, query: users, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: user, color: purple }
activeUsersYear: { title: stats.userActiveYear, query: users, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: user, color: yellow }
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }

View File

@@ -0,0 +1,27 @@
nelmio_api_doc:
models:
use_jms: true
areas:
path_patterns:
- ^/api(?!/doc)
documentation:
#host: '{{hostname}}'
schemes: [http, https]
info:
title: Kimai 2 - API Docs
description: REST API description for the Kimai 2 time-tracking software
version: 0.1
securityDefinitions:
X-AUTH-USER:
type: apiKey
description: 'Value: {Username}'
name: X-AUTH-USER
in: header
X-AUTH-TOKEN:
type: apiKey
description: 'Value: {API Token}'
name: X-AUTH-TOKEN
in: header
security:
- X-AUTH-USER: []
- X-AUTH-TOKEN: []

View File

@@ -0,0 +1,6 @@
jms_serializer:
visitors:
json:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION

View File

@@ -2,7 +2,6 @@ security:
encoders:
App\Entity\User: bcrypt
# https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
@@ -13,39 +12,28 @@ security:
security: false
secured_area:
# this firewall applies to all URLs
pattern: ^/
# make sure only allowed users have access to their time-tracking
user_checker: App\Security\UserChecker
anonymous: true
stateless: false
# but the firewall does not require login on every page
# denying access is done in access_control or in your controllers
anonymous: ~
guard:
authenticators:
- App\Security\TokenAuthenticator
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
# This allows the user to login by submitting a username and password
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
form_login:
# fos user bundle handles the form login
provider: fos_userbundle
# The route name that the login form submits to
check_path: fos_user_security_check
# The name of the route where the login form lives
# When the user tries to access a protected page, they are redirected here
login_path: fos_user_security_login
# Secure the login form against CSRF
# Reference: http://symfony.com/doc/current/cookbook/security/csrf_in_login_form.html
csrf_token_generator: security.csrf.token_manager
logout:
# The route name the user can go to in order to logout
path: fos_user_security_logout
# The name of the route to redirect to after logging out
target: homepage
role_hierarchy:
@@ -60,4 +48,4 @@ security:
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }

View File

@@ -0,0 +1,3 @@
sensio_framework_extra:
router:
annotations: false

View File

@@ -11,15 +11,3 @@ homepage:
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
route: timesheet
permanent: true
fos_user_security:
prefix: /{_locale}
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
fos_user_registration:
prefix: /{_locale}/register
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
fos_user_resetting:
prefix: /{_locale}/resetting
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"

View File

@@ -5,4 +5,9 @@ controllers:
requirements:
_locale: '%app_locales%'
defaults:
_locale: '%locale%'
_locale: '%locale%'
app.api:
resource: '../src/API/'
type: rest
prefix: /api

View File

@@ -0,0 +1,12 @@
# Expose security related features like login and logout
fos_user_security:
prefix: /{_locale}
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
# The features "user registration" and "password-reset" are enabled by default.
#
# You can disable them by setting the config keys in file "config/kimai.yaml":
# - kimai.user.registration: false
# - kimai.user.password_reset: false
#
# The routes for these functions are added dynamically in src/Kernel.php

View File

@@ -0,0 +1,11 @@
# Expose the Kimai API documentation as HTML
app.swagger_ui:
path: /api/doc
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
# Expose the Kimai API documentation as swagger compliant JSON
app.swagger:
path: /api/doc.json
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger }

View File

@@ -0,0 +1,18 @@
App\Entity\Activity:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, project_id]
properties:
id:
include: true
name:
include: true
comment:
include: true
visible:
include: true
virtual_properties:
getProjectId:
name: project_id
serialized_name: project_id
exp: "object.getProject() === null ? null : object.getProject().getId()"
type: integer

View File

@@ -0,0 +1,35 @@
App\Entity\Customer:
exclusion_policy: All
properties:
id:
include: true
name:
include: true
number:
include: true
comment:
include: true
visible:
include: true
company:
include: true
contact:
include: true
address:
include: true
country:
include: true
currency:
include: true
phone:
include: true
fax:
include: true
mobile:
include: true
mail:
include: true
homepage:
include: true
timezone:
include: true

View File

@@ -0,0 +1,22 @@
App\Entity\Project:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer_id]
properties:
id:
include: true
name:
include: true
comment:
include: true
visible:
include: true
budget:
include: true
orderNumber:
include: true
virtual_properties:
getCustomerId:
name: customer_id
serialized_name: customer_id
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
type: integer

View File

@@ -0,0 +1,11 @@
App\Entity\User:
exclusion_policy: All
properties:
id:
include: true
alias:
include: true
title:
include: true
avatar:
include: true

View File

@@ -0,0 +1,23 @@
FOS\UserBundle\Model\User:
exclusion_policy: NONE
properties:
email:
exclude: true
emailCanonical:
exclude: true
usernameCanonical:
exclude: true
password:
exclude: true
plainPassword:
exclude: true
registeredAt:
exclude: true
lastLogin:
exclude: true
confirmationToken:
exclude: true
passwordRequestedAt:
exclude: true
salt:
exclude: true

View File

@@ -2,7 +2,7 @@
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
app_locales: en|de|ru
app_locales: en|de|it|fr|es|ru
services:
# default configuration for services in *this* file
@@ -36,13 +36,24 @@ services:
class: App\Invoice\ServiceInvoice
arguments: ['%kimai.invoice%']
# a route listener, that injects the default locale into the route
App\EventSubscriber\RedirectToLocaleSubscriber:
class: App\EventSubscriber\RedirectToLocaleSubscriber
arguments: ['@router', '%app_locales%', '%locale%']
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
App\Calendar\Service:
arguments:
$config: "%kimai.calendar%"
App\Repository\WidgetRepository:
arguments:
$widgets: "%kimai.widgets%"
App\Controller\DashboardController:
arguments:
$dashboard: "%kimai.dashboard%"
# ================================================================================
# DATABASE
# ================================================================================
@@ -83,7 +94,7 @@ services:
App\Twig\Extensions:
arguments:
$locales: "%app_locales%"
$languages: "%kimai.languages%"
App\Twig\DateExtensions:
arguments:
@@ -101,3 +112,32 @@ services:
App\Timesheet\Calculator\RateCalculator:
arguments: ["%kimai.timesheet.rates%"]
# ================================================================================
# REPOSITORIES
# ================================================================================
App\Repository\TimesheetRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Timesheet']
App\Repository\UserRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\User']
App\Repository\ActivityRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Activity']
App\Repository\ProjectRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Project']
App\Repository\CustomerRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Customer']

View File

@@ -2,8 +2,4 @@ services:
_defaults:
public: true
# If you need to access services in a test, create an alias
# and then fetch that alias from the container. As a convention,
# aliases are prefixed with test. For example:
#
# test.App\Service\MyService: '@App\Service\MyService'
test.PasswordEncoder: "@security.encoder_factory"

View File

@@ -9,11 +9,12 @@
"devDependencies": {
"@fortawesome/fontawesome-free": "~5.1.1",
"@symfony/webpack-encore": "^0.20.1",
"admin-lte": "~2.3.11",
"admin-lte": "~2.4.8",
"bootstrap-sass": "^3.3.7",
"bootstrap-select": "^1.13.1",
"chart.js": "1.1.*",
"chart.js": "~2.7.2",
"daterangepicker": "^3.0.3",
"fullcalendar": "^3.9.0",
"icheck": "^1.0.2",
"jquery": "^3.3.1",
"jquery-slimscroll": "^1.3.8",

View File

@@ -17,6 +17,8 @@
<env name="DATABASE_PREFIX" value="kimai2_" force="true"/>
<!-- define your env variables for the test env here -->
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak_vendors"/>
<!-- ###+ doctrine/doctrine-bundle ### -->
<env name="DATABASE_URL" value="sqlite:///var/data/kimai_test.sqlite" force="true"/>
<!-- ###- doctrine/doctrine-bundle ### -->

View File

@@ -3,7 +3,7 @@
# mod_rewrite). Additionally, this reduces the matching process for the
# start page (path "/") because otherwise Apache will apply the rewriting rules
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
DirectoryIndex app.php
DirectoryIndex index.php
<IfModule mod_rewrite.c>
RewriteEngine On
@@ -30,7 +30,7 @@ DirectoryIndex app.php
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
# following RewriteCond (best solution)
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
# If the requested filename exists, simply serve it.
# We only want to let Apache serve files and not directories.
@@ -38,7 +38,7 @@ DirectoryIndex app.php
RewriteRule .? - [L]
# Rewrite all other queries to the front controller.
RewriteRule .? %{ENV:BASE}/app.php [L]
RewriteRule .? %{ENV:BASE}/index.php [L]
</IfModule>
<IfModule !mod_rewrite.c>
@@ -46,7 +46,7 @@ DirectoryIndex app.php
# When mod_rewrite is not available, we instruct a temporary redirect of
# the start page to the front controller explicitly so that the website
# and the generated links can still be used.
RedirectMatch 302 ^/$ /app.php/
RedirectMatch 302 ^/$ /index.php/
# RedirectTemp cannot be used instead
</IfModule>
</IfModule>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -1,9 +1,8 @@
{
"build/app.js": "/build/app.js?df75851c817558ca8803",
"build/app.css": "/build/app.css?b1944e8d2023603075ed8d913c21a8e8",
"build/app.js": "/build/app.js?a67489c4fc07f30e6f86",
"build/app.css": "/build/app.css?e2bd74ce6119917ab0c8fd4de8d4b9c6",
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
"build/images/boxed-bg.jpg": "/build/images/boxed-bg.jpg?7799dece",
"build/images/blue.png": "/build/images/blue.png?96f8a905",
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
"build/images/fa-solid-900.svg": "/build/images/fa-solid-900.svg?666a82cb",
"build/images/glyphicons-halflings-regular.svg": "/build/images/glyphicons-halflings-regular.svg?89889688",
@@ -14,7 +13,7 @@
"build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.woff?e9f83339",
"build/fonts/glyphicons-halflings-regular.woff2": "/build/fonts/glyphicons-halflings-regular.woff2?448c34a5",
"build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.eot?0b93480e",
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
"build/images/blue.png": "/build/images/blue.png?96f8a905",
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.woff2?930c1264",
"build/images/fa-regular-400.svg": "/build/images/fa-regular-400.svg?4e41caaf",

View File

@@ -0,0 +1,89 @@
<?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\API;
use App\Entity\Activity;
use App\Repository\ActivityRepository;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation as API;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* @RouteResource("Activity")
*
* @Security("is_granted('ROLE_USER')")
*/
class ActivityController extends Controller
{
/**
* @var ActivityRepository
*/
protected $repository;
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
* @param ActivityRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
}
/**
* @SWG\Response(
* response=200,
* description="Returns the collection of all existing activities",
* @SWG\Schema(ref=@API\Model(type=Activity::class)),
* )
*
* @return Response
*/
public function cgetAction()
{
$data = $this->repository->findAll();
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
/**
* @SWG\Response(
* response=200,
* description="Returns one activity entity",
* @SWG\Schema(ref=@API\Model(type=Activity::class)),
* )
*
* @param int $id
* @return Response
*/
public function getAction($id)
{
$data = $this->repository->find($id);
if (null === $data) {
throw new NotFoundException();
}
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
}

View File

@@ -0,0 +1,89 @@
<?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\API;
use App\Entity\Customer;
use App\Repository\CustomerRepository;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* @RouteResource("Customer")
*
* @Security("is_granted('ROLE_USER')")
*/
class CustomerController extends Controller
{
/**
* @var CustomerRepository
*/
protected $repository;
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
* @param CustomerRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
}
/**
* @SWG\Response(
* response=200,
* description="Returns the collection of all existing customer",
* @SWG\Schema(ref=@Model(type=Customer::class)),
* )
*
* @return Response
*/
public function cgetAction()
{
$data = $this->repository->findAll();
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
/**
* @SWG\Response(
* response=200,
* description="Returns one customer entity",
* @SWG\Schema(ref=@Model(type=Customer::class)),
* )
*
* @param int $id
* @return Response
*/
public function getAction($id)
{
$data = $this->repository->find($id);
if (null === $data) {
throw new NotFoundException();
}
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
}

View 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\API;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Swagger\Annotations as SWG;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class HealthcheckController extends Controller
{
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
*/
public function __construct(ViewHandlerInterface $viewHandler)
{
$this->viewHandler = $viewHandler;
}
/**
* @SWG\Response(
* response=200,
* description="A simple route that returns a 'pong', which you can use for testing the API",
* )
*
* @Rest\Get(path="/ping")
*/
public function pingAction()
{
$view = new View(['message' => 'pong'], 200);
return $this->viewHandler->handle($view);
}
}

View File

@@ -0,0 +1,22 @@
<?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\API;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class NotFoundException extends NotFoundHttpException
{
public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 0, array $headers = [])
{
parent::__construct($message, $previous, $code, $headers);
}
}

View File

@@ -0,0 +1,89 @@
<?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\API;
use App\Entity\Project;
use App\Repository\ProjectRepository;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* @RouteResource("Project")
*
* @Security("is_granted('ROLE_USER')")
*/
class ProjectController extends Controller
{
/**
* @var ProjectRepository
*/
protected $repository;
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
* @param ProjectRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
}
/**
* @SWG\Response(
* response=200,
* description="Returns the collection of all existing projects",
* @SWG\Schema(ref=@Model(type=Project::class)),
* )
*
* @return Response
*/
public function cgetAction()
{
$data = $this->repository->findAll();
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
/**
* @SWG\Response(
* response=200,
* description="Returns one project entity",
* @SWG\Schema(ref=@Model(type=Project::class)),
* )
*
* @param int $id
* @return Response
*/
public function getAction($id)
{
$data = $this->repository->find($id);
if (null === $data) {
throw new NotFoundException();
}
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
}

View File

@@ -0,0 +1,90 @@
<?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\API;
use App\Entity\User;
use App\Repository\UserRepository;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* @RouteResource("User")
*
* @Security("is_granted('ROLE_SUPER_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
class UserController extends Controller
{
/**
* @var UserRepository
*/
protected $repository;
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
/**
* @param ViewHandlerInterface $viewHandler
* @param UserRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
}
/**
* @SWG\Response(
* response=200,
* description="Returns the collection of all registered users",
* @SWG\Schema(ref=@Model(type=User::class)),
* )
*
* @return Response
*/
public function cgetAction()
{
$data = $this->repository->findAll();
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
/**
* @SWG\Response(
* response=200,
* description="Return one user entity",
* @SWG\Schema(ref=@Model(type=User::class)),
* )
*
* @param int $id
* @return Response
*/
public function getAction($id)
{
$data = $this->repository->find($id);
if (null === $data) {
throw new NotFoundException();
}
$view = new View($data, 200);
return $this->viewHandler->handle($view);
}
}

66
src/Calendar/Config.php Normal file
View File

@@ -0,0 +1,66 @@
<?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\Calendar;
class Config
{
/**
* @var array
*/
protected $config = [];
/**
* @param array $config
*/
public function __construct(array $config)
{
$this->config = $config;
}
/**
* @return array
*/
public function getBusinessDays()
{
return $this->config['businessHours']['days'];
}
/**
* @return string
*/
public function getBusinessTimeBegin()
{
return $this->config['businessHours']['begin'];
}
/**
* @return string
*/
public function getBusinessTimeEnd()
{
return $this->config['businessHours']['end'];
}
/**
* @return int
*/
public function getDayLimit()
{
return $this->config['day_limit'];
}
/**
* @return bool
*/
public function isShowWeekNumbers()
{
return $this->config['week_numbers'];
}
}

48
src/Calendar/Google.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Calendar;
class Google
{
/**
* @var Source[]
*/
protected $sources = [];
/**
* @var string
*/
protected $apiKey = null;
/**
* @param string $apiKey
* @param Source[] $sources
*/
public function __construct($apiKey, $sources = [])
{
$this->apiKey = $apiKey;
$this->sources = $sources;
}
/**
* @return Source[]
*/
public function getSources()
{
return $this->sources;
}
/**
* @return string
*/
public function getApiKey()
{
return $this->apiKey;
}
}

59
src/Calendar/Service.php Normal file
View File

@@ -0,0 +1,59 @@
<?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\Calendar;
class Service
{
/**
* @var array
*/
protected $config;
/**
* Service constructor.
* @param array $config
*/
public function __construct(array $config)
{
$this->config = $config;
}
/**
* @return Config
*/
public function getConfig()
{
return new Config($this->config);
}
/**
* @return Google
*/
public function getGoogle()
{
$apiKey = $this->config['google']['api_key'] ?? null;
$sources = [];
if (isset($this->config['google']['sources'])) {
foreach ($this->config['google']['sources'] as $name => $config) {
$source = new Source();
$source
->setColor($config['color'])
->setUri($config['id'])
->setId($name)
;
$sources[] = $source;
}
}
return new Google($apiKey, $sources);
}
}

83
src/Calendar/Source.php Normal file
View File

@@ -0,0 +1,83 @@
<?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\Calendar;
class Source
{
/**
* @var string
*/
protected $id;
/**
* @var string
*/
protected $uri;
/**
* @var string
*/
protected $color;
/**
* @return string
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @param string $id
* @return Source
*/
public function setId(string $id)
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getUri(): ?string
{
return $this->uri;
}
/**
* @param string $uri
* @return Source
*/
public function setUri(string $uri)
{
$this->uri = $uri;
return $this;
}
/**
* @return string
*/
public function getColor(): ?string
{
return $this->color;
}
/**
* @param string $color
* @return Source
*/
public function setColor(string $color)
{
$this->color = $color;
return $this;
}
}

View File

@@ -0,0 +1,268 @@
<?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\Calendar;
use App\Entity\Timesheet;
class TimesheetEntity
{
/**
* @var int
*/
protected $id;
/**
* @var \DateTime
*/
protected $start;
/**
* @var \DateTime|null
*/
protected $end;
/**
* @var string
*/
protected $title;
/**
* @var string
*/
protected $description;
/**
* @var string
*/
protected $customer;
/**
* @var string
*/
protected $project;
/**
* @var string
*/
protected $activity;
/**
* @var string|null
*/
protected $borderColor;
/**
* @var string|null
*/
protected $backgroundColor;
/**
* @param Timesheet $entry
*/
public function __construct(Timesheet $entry)
{
$this->id = $entry->getId();
$this->start = $entry->getBegin();
$this->title = $entry->getActivity()->getName();
$this->description = $entry->getDescription();
$this->customer = $entry->getActivity()->getProject()->getCustomer()->getName();
$this->project = $entry->getActivity()->getProject()->getName();
$this->activity = $entry->getActivity()->getName();
if (null === $entry->getEnd()) {
// TODO move these colors to the controller
$this->borderColor = '#f39c12';
$this->backgroundColor = '#f39c12';
} else {
$this->end = $entry->getEnd();
}
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
* @return TimesheetEntity
*/
public function setId(int $id)
{
$this->id = $id;
return $this;
}
/**
* @return \DateTime
*/
public function getStart(): \DateTime
{
return $this->start;
}
/**
* @param \DateTime $start
* @return TimesheetEntity
*/
public function setStart(\DateTime $start)
{
$this->start = $start;
return $this;
}
/**
* @return \DateTime|null
*/
public function getEnd(): ?\DateTime
{
return $this->end;
}
/**
* @param \DateTime|null $end
* @return TimesheetEntity
*/
public function setEnd(?\DateTime $end)
{
$this->end = $end;
return $this;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @param string $title
* @return TimesheetEntity
*/
public function setTitle(string $title)
{
$this->title = $title;
return $this;
}
/**
* @return string
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string $description
* @return TimesheetEntity
*/
public function setDescription(string $description)
{
$this->description = $description;
return $this;
}
/**
* @return string
*/
public function getCustomer(): string
{
return $this->customer;
}
/**
* @param string $customer
* @return TimesheetEntity
*/
public function setCustomer(string $customer)
{
$this->customer = $customer;
return $this;
}
/**
* @return string
*/
public function getProject(): string
{
return $this->project;
}
/**
* @param string $project
* @return TimesheetEntity
*/
public function setProject(string $project)
{
$this->project = $project;
return $this;
}
/**
* @return string
*/
public function getActivity(): string
{
return $this->activity;
}
/**
* @param string $activity
* @return TimesheetEntity
*/
public function setActivity(string $activity)
{
$this->activity = $activity;
return $this;
}
/**
* @return null|string
*/
public function getBorderColor(): ?string
{
return $this->borderColor;
}
/**
* @param null|string $borderColor
* @return TimesheetEntity
*/
public function setBorderColor(?string $borderColor)
{
$this->borderColor = $borderColor;
return $this;
}
/**
* @return null|string
*/
public function getBackgroundColor(): ?string
{
return $this->backgroundColor;
}
/**
* @param null|string $backgroundColor
* @return TimesheetEntity
*/
public function setBackgroundColor(?string $backgroundColor)
{
$this->backgroundColor = $backgroundColor;
return $this;
}
}

View File

@@ -16,15 +16,14 @@ use App\Form\Toolbar\ActivityToolbarForm;
use App\Repository\Query\ActivityQuery;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage activities in the admin part of the site.
*
* @Route("/admin/activity")
* @Route(path="/admin/activity")
* @Security("is_granted('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
@@ -39,9 +38,8 @@ class ActivityController extends AbstractController
}
/**
* @Route("/", defaults={"page": 1}, name="admin_activity")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated")
* @Method("GET")
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
* @Cache(smaxage="10")
*/
public function indexAction($page, Request $request)
@@ -68,8 +66,7 @@ class ActivityController extends AbstractController
}
/**
* @Route("/create", name="admin_activity_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
@@ -80,8 +77,7 @@ class ActivityController extends AbstractController
}
/**
* @Route("/{id}/edit", name="admin_activity_edit")
* @Method({"GET", "POST"})
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', activity)")
*
* @param Activity $activity
@@ -96,8 +92,7 @@ class ActivityController extends AbstractController
/**
* The route to delete an existing entry.
*
* @Route("/{id}/delete", name="admin_activity_delete")
* @Method({"GET", "POST"})
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', activity)")
*
* @param Activity $activity
@@ -122,7 +117,7 @@ class ActivityController extends AbstractController
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
return $this->redirectToRoute('admin_activity');
}
return $this->render(
@@ -160,7 +155,7 @@ class ActivityController extends AbstractController
$editForm->get('create_more')->setData(true);
$activity = $newActivity;
} else {
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
return $this->redirectToRoute('admin_activity');
}
}

View File

@@ -15,15 +15,14 @@ use App\Form\CustomerEditForm;
use App\Form\Toolbar\CustomerToolbarForm;
use App\Repository\Query\CustomerQuery;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage activities in the admin part of the site.
*
* @Route("/admin/customer")
* @Route(path="/admin/customer")
* @Security("is_granted('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
@@ -38,9 +37,8 @@ class CustomerController extends AbstractController
}
/**
* @Route("/", defaults={"page": 1}, name="admin_customer")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated")
* @Method("GET")
* @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"})
*/
public function indexAction($page, Request $request)
{
@@ -65,8 +63,7 @@ class CustomerController extends AbstractController
}
/**
* @Route("/create", name="admin_customer_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
*/
public function createAction(Request $request)
{
@@ -74,8 +71,7 @@ class CustomerController extends AbstractController
}
/**
* @Route("/{id}/edit", name="admin_customer_edit")
* @Method({"GET", "POST"})
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', customer)")
*/
public function editAction(Customer $customer, Request $request)
@@ -101,7 +97,7 @@ class CustomerController extends AbstractController
$this->flashSuccess('action.updated_successfully');
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
return $this->redirectToRoute('admin_customer');
}
return $this->render('admin/customer_edit.html.twig', [
@@ -113,8 +109,7 @@ class CustomerController extends AbstractController
/**
* The route to delete an existing entry.
*
* @Route("/{id}/delete", name="admin_customer_delete")
* @Method({"GET", "POST"})
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', customer)")
*
* @param Customer $customer
@@ -139,7 +134,7 @@ class CustomerController extends AbstractController
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
return $this->redirectToRoute('admin_customer');
}
return $this->render('admin/customer_delete.html.twig', [

View File

@@ -17,15 +17,14 @@ use App\Form\Toolbar\ProjectToolbarForm;
use App\Repository\Query\ProjectQuery;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage projects in the admin part of the site.
*
* @Route("/admin/project")
* @Route(path="/admin/project")
* @Security("is_granted('ROLE_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
@@ -40,9 +39,8 @@ class ProjectController extends AbstractController
}
/**
* @Route("/", defaults={"page": 1}, name="admin_project")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated")
* @Method("GET")
* @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"})
* @Cache(smaxage="10")
*/
public function indexAction($page, Request $request)
@@ -69,8 +67,7 @@ class ProjectController extends AbstractController
}
/**
* @Route("/create", name="admin_project_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
*/
public function createAction(Request $request)
{
@@ -78,8 +75,7 @@ class ProjectController extends AbstractController
}
/**
* @Route("/{id}/edit", name="admin_project_edit")
* @Method({"GET", "POST"})
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', project)")
*/
public function editAction(Project $project, Request $request)
@@ -90,8 +86,7 @@ class ProjectController extends AbstractController
/**
* The route to delete an existing entry.
*
* @Route("/{id}/delete", name="admin_project_delete")
* @Method({"GET", "POST"})
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', project)")
*
* @param Project $project
@@ -116,7 +111,7 @@ class ProjectController extends AbstractController
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
return $this->redirectToRoute('admin_project');
}
return $this->render('admin/project_delete.html.twig', [
@@ -151,7 +146,7 @@ class ProjectController extends AbstractController
$editForm->get('create_more')->setData(true);
$project = $newProject;
} else {
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
return $this->redirectToRoute('admin_project');
}
}

View File

@@ -16,15 +16,14 @@ use App\Form\TimesheetEditForm;
use App\Form\Toolbar\TimesheetAdminToolbarForm;
use App\Repository\Query\TimesheetQuery;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used for manage timesheet entries in the admin part of the site.
*
* @Route("/team/timesheet")
* @Route(path="/team/timesheet")
* @Security("is_granted('ROLE_TEAMLEAD')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
@@ -44,9 +43,8 @@ class TimesheetController extends AbstractController
/**
* This route shows all users timesheet entries.
*
* @Route("/", defaults={"page": 1}, name="admin_timesheet")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated")
* @Method("GET")
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
*
* @param $page
* @param Request $request
@@ -78,8 +76,7 @@ class TimesheetController extends AbstractController
/**
* The route to stop a running entry.
*
* @Route("/{id}/stop", name="admin_timesheet_stop")
* @Method({"GET"})
* @Route(path="/{id}/stop", name="admin_timesheet_stop", methods={"GET"})
* @Security("is_granted('stop', entry)")
*
* @param Timesheet $entry
@@ -93,8 +90,7 @@ class TimesheetController extends AbstractController
/**
* The route to edit an existing entry.
*
* @Route("/{id}/edit", name="admin_timesheet_edit")
* @Method({"GET", "POST"})
* @Route(path="/{id}/edit", name="admin_timesheet_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', entry)")
*
* @param Timesheet $entry
@@ -109,8 +105,7 @@ class TimesheetController extends AbstractController
/**
* The route to create a new entry by form.
*
* @Route("/create", name="admin_timesheet_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="admin_timesheet_create", methods={"GET", "POST"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
@@ -123,8 +118,7 @@ class TimesheetController extends AbstractController
/**
* The route to delete an existing entry.
*
* @Route("/{id}/delete", name="admin_timesheet_delete")
* @Method({"GET", "POST"})
* @Route(path="/{id}/delete", name="admin_timesheet_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', entry)")
*
* @param Timesheet $entry

View File

@@ -10,29 +10,54 @@
namespace App\Controller\Admin;
use App\Controller\AbstractController;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Form\Toolbar\UserToolbarForm;
use App\Form\UserCreateType;
use App\Repository\Query\UserQuery;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* Controller used to manage users in the admin part of the site.
*
* @Route("/admin/user")
* @Route(path="/admin/user")
* @Security("is_granted('ROLE_SUPER_ADMIN')")
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
class UserController extends AbstractController
{
/**
* @Route("/", defaults={"page": 1}, name="admin_user")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated")
* @Method("GET")
* @var UserPasswordEncoderInterface
*/
protected $encoder;
/**
* @param UserPasswordEncoderInterface $encoder
*/
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
/**
* @return \App\Repository\UserRepository
*/
protected function getRepository()
{
return $this->getDoctrine()->getRepository(User::class);
}
/**
* @Route(path="/", defaults={"page": 1}, name="admin_user", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated", methods={"GET"})
* @param int $page
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function indexAction($page, Request $request)
{
@@ -47,7 +72,7 @@ class UserController extends AbstractController
}
/* @var $entries Pagerfanta */
$entries = $this->getDoctrine()->getRepository(User::class)->findByQuery($query);
$entries = $this->getRepository()->findByQuery($query);
return $this->render('admin/user.html.twig', [
'entries' => $entries,
@@ -57,8 +82,7 @@ class UserController extends AbstractController
}
/**
* @Route("/create", name="admin_user_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
* @Security("is_granted('create', user)")
*/
public function createAction(Request $request)
@@ -69,8 +93,7 @@ class UserController extends AbstractController
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$password = $this->get('security.password_encoder')
->encodePassword($user, $user->getPlainPassword());
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$user->setEnabled(true);
$user->setRoles([User::DEFAULT_ROLE]);
@@ -99,6 +122,48 @@ class UserController extends AbstractController
);
}
/**
* The route to delete an existing user.
*
* @Route(path="/{id}/delete", name="admin_user_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', userToDelete)")
*
* @param User $userToDelete
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function deleteAction(User $userToDelete, Request $request)
{
$stats = $this->getDoctrine()->getRepository(Timesheet::class)->getUserStatistics($userToDelete);
$deleteForm = $this->createFormBuilder()
->setAction($this->generateUrl('admin_user_delete', ['id' => $userToDelete->getId()]))
->setMethod('POST')
->getForm();
$deleteForm->handleRequest($request);
if (0 == $stats->getRecordsTotal() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($userToDelete);
$entityManager->flush();
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_user');
}
return $this->render(
'admin/user_delete.html.twig',
[
'user' => $userToDelete,
'stats' => $stats,
'form' => $deleteForm->createView(),
]
);
}
/**
* @param UserQuery $query
* @return \Symfony\Component\Form\FormInterface

View File

@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use App\Calendar\Service;
use App\Calendar\TimesheetEntity;
use App\Entity\Timesheet;
use App\Repository\Query\TimesheetQuery;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to display calendars.
*
* @Route(path="/calendar")
* @Security("is_granted('ROLE_USER')")
*/
class CalendarController extends AbstractController
{
/**
* @var Service
*/
protected $calendar;
/**
* @param Service $calendar
*/
public function __construct(Service $calendar)
{
$this->calendar = $calendar;
}
/**
* @Route(path="/", name="calendar", methods={"GET"})
* @Cache(smaxage="10")
*/
public function userCalendar()
{
return $this->render('calendar/user.html.twig', [
'config' => $this->calendar->getConfig(),
'google' => $this->calendar->getGoogle()
]);
}
/**
* @Route(path="/user", name="calendar_entries", methods={"GET"})
* @Cache(smaxage="10")
*/
public function calendarEntries(Request $request)
{
$start = $request->get('start');
$end = $request->get('end');
$start = \DateTime::createFromFormat('Y-m-d', $start);
if ($start === false) {
$start = new \DateTime('first day of this month');
}
$start->setTime(0, 0, 0);
$end = \DateTime::createFromFormat('Y-m-d', $end);
if ($end === false) {
$end = clone $start;
$end = $end->modify('last day of this month');
}
$end->setTime(23, 59, 59);
$query = new TimesheetQuery();
$query
->setBegin($start)
->setUser($this->getUser())
->setState(TimesheetQuery::STATE_ALL)
->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER)
;
// running entries should only occur for the current month, but they won't
// be found if we add the end to the query
if ((new \DateTime())->getTimestamp() > $end->getTimestamp()) {
$query->setEnd($end);
}
$repository = $this->getDoctrine()->getRepository(Timesheet::class);
/* @var $entries Timesheet[] */
$entries = $repository->findByQuery($query)->getQuery()->execute();
$result = [];
foreach ($entries as $entry) {
$result[] = new TimesheetEntity($entry);
}
return $this->json($result);
}
}

View File

@@ -9,139 +9,100 @@
namespace App\Controller;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Repository\Query\TimesheetQuery;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use App\Event\DashboardEvent;
use App\Model\DashboardSection;
use App\Repository\WidgetRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Annotation\Route;
/**
* Dashboard controller for the admin area.
*
* @Route("/dashboard")
* @Route(path="/dashboard")
* @Security("is_granted('ROLE_USER')")
*/
class DashboardController extends Controller
{
/**
* @Route("/", defaults={}, name="dashboard")
* @Method("GET")
* @var EventDispatcherInterface
*/
public function indexAction()
protected $eventDispatcher;
/**
* @var WidgetRepository
*/
protected $repository;
/**
* @var array
*/
protected $dashboard;
/**
* @param EventDispatcherInterface $dispatcher
* @param WidgetRepository $repository
* @param array $dashboard
*/
public function __construct(EventDispatcherInterface $dispatcher, WidgetRepository $repository, array $dashboard)
{
$user = $this->getUser();
$userStats = $this->getDoctrine()->getRepository(User::class)->getGlobalStatistics();
// FIXME move the other widgets to the Kimai, the inheritence is wrong as Kimai
// shouldn't know about Timesheets
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
$timesheetUserStats = $timesheetRepo->getUserStatistics($user);
$timesheetGlobalStats = $timesheetRepo->getGlobalStatistics();
$activityStats = $this->getDoctrine()->getRepository(Activity::class)->getGlobalStatistics();
$projectStats = $this->getDoctrine()->getRepository(Project::class)->getGlobalStatistics();
$customerStats = $this->getDoctrine()->getRepository(Customer::class)->getGlobalStatistics();
return $this->render('dashboard/index.html.twig', [
'dashboard_widgets' => $this->getWidgets(),
'timesheetGlobal' => $timesheetGlobalStats,
'timesheetUser' => $timesheetUserStats,
'activity' => $activityStats,
'project' => $projectStats,
'customer' => $customerStats,
'user' => $userStats,
]);
$this->eventDispatcher = $dispatcher;
$this->repository = $repository;
$this->dashboard = $dashboard;
}
/**
* colors: blue / yellow / purple / green / black
* icons: bar-chart / line-chart / calendar / clock
*
* @return array
* @Route(path="/", defaults={}, name="dashboard", methods={"GET"})
*/
protected function getWidgets()
public function indexAction()
{
// @codingStandardsIgnoreStart
$widgets = [
/*
[
'header' => 'dashboard.you',
'widgets' => [
"{{ widgets.info_box_progress('Bewilligte Stunden', 'Stunden zur Abrechnung bewilligt', 120, 10, 'star') }}",
"{{ widgets.info_box_progress('Umsatz / Monat', '70% Increase in 30 Days', 6830, 30, 'credit-card', 'black') }}",
"{{ widgets.info_box_progress('Stunden persönlich', 'Das ist noch nicht genug', 135, 60, 'hourglass') }}",
"{{ widgets.info_box_progress('Anzahl Benutzer', 'Mehr ist besser!', 5, 90, 'user') }}",
],
],
*/
[
'id' => 'profile.stats',
'header' => 'dashboard.you',
'widgets' => [
"{{ widgets.info_box_counter('stats.durationThisMonth', timesheetUser.durationThisMonth|duration(true), 'far fa-hourglass', 'green') }}",
//"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetUser.amountThisMonth|money, 'money', 'blue') }}",
"{{ widgets.info_box_counter('stats.durationTotal', timesheetUser.durationTotal|duration(true), 'far fa-hourglass', 'red') }}",
//"{{ widgets.info_box_counter('stats.amountTotal', timesheetUser.amountTotal|money, 'money', 'yellow') }}",
],
],
];
$event = new DashboardEvent($this->getUser());
if (!$this->isGranted('ROLE_TEAMLEAD', null)) {
return $widgets;
foreach ($this->dashboard as $widgetRow) {
if (empty($widgetRow['widgets'])) {
continue;
}
if (!$this->isGranted($widgetRow['permission'])) {
continue;
}
$row = new DashboardSection($widgetRow['title'] ?? null);
$row
->setOrder($widgetRow['order'])
->setType($widgetRow['type'])
;
foreach ($widgetRow['widgets'] as $widgetName) {
if (!$this->repository->has($widgetName)) {
throw new \Exception('Unknwon widget: ' . $widgetName);
}
$row->addWidget($this->repository->get($widgetName, $event->getUser()));
}
$event->addSection($row);
}
$widgets[] = [
'id' => 'alluser.stats',
'header' => 'dashboard.all',
'widgets' => [
"{{ widgets.info_box_counter('stats.durationThisMonth', timesheetGlobal.durationThisMonth|duration(true), 'far fa-hourglass', 'blue') }}",
"{{ widgets.info_box_counter('stats.durationTotal', timesheetGlobal.durationTotal|duration(true), 'far fa-hourglass', 'yellow') }}",
"{{ widgets.info_box_counter('stats.activeRecordings', timesheetGlobal.activeCurrently, 'far fa-hourglass', 'red', path('admin_timesheet', {'state': " . TimesheetQuery::STATE_RUNNING . '})) }}',
],
];
$this->eventDispatcher->dispatch(
DashboardEvent::DASHBOARD,
$event
);
$widgets[] = [
'id' => 'user.stats',
'header' => '',
'widgets' => [
"{{ widgets.info_box_counter('stats.userTotal', user.totalAmount, 'user', 'red') }}",
"{{ widgets.info_box_counter('stats.userActiveThisMoth', timesheetGlobal.activeThisMonth, 'user', 'yellow') }}",
"{{ widgets.info_box_counter('stats.userActiveEver', timesheetGlobal.activeTotal, 'user', 'blue') }}",
],
];
$sections = $event->getSections();
if (!$this->isGranted('ROLE_ADMIN', null)) {
return $widgets;
}
uasort(
$sections,
function (DashboardSection $a, DashboardSection $b) {
if ($a->getOrder() == $b->getOrder()) {
return 0;
}
$widgets[] = [
'id' => 'alluser.money_stats',
'header' => '',
'widgets' => [
"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetGlobal.amountThisMonth|money, 'far fa-money-bill-alt', 'green') }}",
"{{ widgets.info_box_counter('stats.amountTotal', timesheetGlobal.amountTotal|money, 'far fa-money-bill-alt', 'red') }}",
],
];
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
}
);
$widgets[] = [
'id' => 'admin.stats',
'header' => 'dashboard.admin',
'widgets' => [
"{{ widgets.info_box_more('stats.userTotal', user.totalAmount, ' ', path('admin_user'), 'user') }}",
"{{ widgets.info_box_more('stats.customerTotal', customer.count, '', path('admin_customer'), 'customer', 'blue') }}",
"{{ widgets.info_box_more('stats.projectsTotal', project.count, '', path('admin_project'), 'project', 'yellow') }}",
"{{ widgets.info_box_more('stats.activitiesTotal', activity.count, '', path('admin_activity'), 'activity', 'purple') }}",
],
];
// @codingStandardsIgnoreEnd
return $widgets;
return $this->render('dashboard/index.html.twig', [
'widget_rows' => $sections
]);
}
}

View File

@@ -10,15 +10,14 @@
namespace App\Controller;
use App\Constants;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
/**
* This controller can render the markdown documentation from /var/docs/
*
* @Route("/help")
* @Route(path="/help")
* @Security("is_granted('ROLE_USER')")
*/
class HelpController extends Controller
@@ -41,9 +40,8 @@ class HelpController extends Controller
}
/**
* @Route("/", defaults={"chapter": "README"}, name="help")
* @Route("/{chapter}", requirements={"chapter": "[a-zA-Z]*"}, name="help_chapter")
* @Method("GET")
* @Route(path="/", defaults={"chapter": "README"}, name="help", methods={"GET"})
* @Route(path="/{chapter}", requirements={"chapter": "[a-zA-Z]*"}, name="help_chapter", methods={"GET"})
*
* @param string $chapter
* @return \Symfony\Component\HttpFoundation\Response

View File

@@ -19,15 +19,14 @@ use App\Repository\Query\BaseQuery;
use App\Repository\Query\InvoiceQuery;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage invoices.
*
* @Route("/invoice")
* @Route(path="/invoice")
* @Security("is_granted('ROLE_TEAMLEAD')")
*/
class InvoiceController extends AbstractController
@@ -38,7 +37,6 @@ class InvoiceController extends AbstractController
protected $service;
/**
* InvoiceController constructor.
* @param ServiceInvoice $service
*/
public function __construct(ServiceInvoice $service)
@@ -73,7 +71,7 @@ class InvoiceController extends AbstractController
}
/**
* @Route("/", name="invoice")
* @Route(path="/", name="invoice", methods={"GET", "POST"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
@@ -139,9 +137,8 @@ class InvoiceController extends AbstractController
}
/**
* @Route("/template", defaults={"page": 1}, name="admin_invoice_template")
* @Route("/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated")
* @Method({"GET", "POST"})
* @Route(path="/template", defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
* @Route(path="/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated", methods={"GET", "POST"})
*
* TODO permission
*
@@ -159,8 +156,7 @@ class InvoiceController extends AbstractController
}
/**
* @Route("/{id}/edit", name="admin_invoice_template_edit")
* @Method({"GET", "POST"})
* @Route(path="/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
*
* TODO permission
*
@@ -174,8 +170,7 @@ class InvoiceController extends AbstractController
}
/**
* @Route("/create", name="admin_invoice_template_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="admin_invoice_template_create", methods={"GET", "POST"})
*
* TODO permission
*

View File

@@ -11,29 +11,42 @@ namespace App\Controller;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Form\UserApiTokenType;
use App\Form\UserEditType;
use App\Form\UserPasswordType;
use App\Form\UserPreferencesForm;
use App\Form\UserRolesType;
use App\Repository\TimesheetRepository;
use App\Voter\UserVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* User profile controller
*
* @Route("/profile")
* @Route(path="/profile")
* @Security("is_granted('ROLE_USER')")
*/
class ProfileController extends AbstractController
{
/**
* @Route("/{username}", name="user_profile")
* @Method("GET")
* @var UserPasswordEncoderInterface
*/
protected $encoder;
/**
* @param UserPasswordEncoderInterface $encoder
*/
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
/**
* @Route(path="/{username}", name="user_profile", methods={"GET"})
* @Security("is_granted('view', profile)")
*/
public function indexAction(User $profile)
@@ -42,8 +55,7 @@ class ProfileController extends AbstractController
}
/**
* @Route("/{username}/edit", name="user_profile_edit")
* @Method({"GET", "POST"})
* @Route(path="/{username}/edit", name="user_profile_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', profile)")
*/
public function editAction(User $profile, Request $request)
@@ -65,8 +77,7 @@ class ProfileController extends AbstractController
}
/**
* @Route("/{username}/password", name="user_profile_password")
* @Method({"GET", "POST"})
* @Route(path="/{username}/password", name="user_profile_password", methods={"GET", "POST"})
* @Security("is_granted('password', profile)")
*/
public function passwordAction(User $profile, Request $request)
@@ -75,8 +86,7 @@ class ProfileController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $this->get('security.password_encoder')
->encodePassword($profile, $profile->getPlainPassword());
$password = $this->encoder->encodePassword($profile, $profile->getPlainPassword());
$profile->setPassword($password);
$entityManager = $this->getDoctrine()->getManager();
@@ -92,8 +102,32 @@ class ProfileController extends AbstractController
}
/**
* @Route("/{username}/roles", name="user_profile_roles")
* @Method({"GET", "POST"})
* @Route(path="/{username}/api-token", name="user_profile_api_token", methods={"GET", "POST"})
* @Security("is_granted('api-token', profile)")
*/
public function apiTokenAction(User $profile, Request $request)
{
$form = $this->createApiTokenForm($profile);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $this->encoder->encodePassword($profile, $profile->getPlainApiToken());
$profile->setApiToken($password);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($profile);
$entityManager->flush();
$this->flashSuccess('action.updated_successfully');
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'api-token', null, null, null, null, $form);
}
/**
* @Route(path="/{username}/roles", name="user_profile_roles", methods={"GET", "POST"})
* @Security("is_granted('roles', profile)")
*/
public function rolesAction(User $profile, Request $request)
@@ -115,8 +149,7 @@ class ProfileController extends AbstractController
}
/**
* @Route("/{username}/prefs", name="user_profile_preferences")
* @Method({"GET", "POST"})
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
* @Security("is_granted('preferences', profile)")
*/
public function savePreferencesAction(User $profile, Request $request)
@@ -147,11 +180,9 @@ class ProfileController extends AbstractController
}
}
foreach ($preferences as $preference) {
$preference->setUser($profile);
$entityManager->persist($preference);
$entityManager->flush();
}
$profile->setPreferences($preferences);
$entityManager->persist($profile);
$entityManager->flush();
$this->flashSuccess('action.updated_successfully');
@@ -168,6 +199,7 @@ class ProfileController extends AbstractController
* @param Form|null $pwdForm
* @param Form|null $rolesForm
* @param Form|null $prefsForm
* @param Form|null $apiTokenForm
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Doctrine\ORM\NonUniqueResultException
*/
@@ -177,7 +209,8 @@ class ProfileController extends AbstractController
Form $editForm = null,
Form $pwdForm = null,
Form $rolesForm = null,
Form $prefsForm = null
Form $prefsForm = null,
Form $apiTokenForm = null
) {
/* @var $timesheetRepo TimesheetRepository */
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
@@ -200,6 +233,10 @@ class ProfileController extends AbstractController
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
$viewVars['forms']['password'] = $pwdForm->createView();
}
if ($this->isGranted(UserVoter::API_TOKEN, $user)) {
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
$viewVars['forms']['api-token'] = $apiTokenForm->createView();
}
if ($this->isGranted(UserVoter::ROLES, $user)) {
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
$viewVars['forms']['roles'] = $rolesForm->createView();
@@ -276,4 +313,21 @@ class ProfileController extends AbstractController
]
);
}
/**
* @param User $user
* @return \Symfony\Component\Form\FormInterface
*/
private function createApiTokenForm(User $user)
{
return $this->createForm(
UserApiTokenType::class,
$user,
[
'validation_groups' => ['apiTokenUpdate'],
'action' => $this->generateUrl('user_profile_api_token', ['username' => $user->getUsername()]),
'method' => 'POST'
]
);
}
}

View File

@@ -16,15 +16,14 @@ use App\Form\Toolbar\TimesheetToolbarForm;
use App\Repository\Query\TimesheetQuery;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to manage timesheet contents in the public part of the site.
* Controller used to manage timesheets.
*
* @Route("/timesheet")
* @Route(path="/timesheet")
* @Security("is_granted('ROLE_USER')")
*/
class TimesheetController extends AbstractController
@@ -32,7 +31,6 @@ class TimesheetController extends AbstractController
use TimesheetControllerTrait;
/**
* TimesheetController constructor.
* @param bool $durationOnly
*/
public function __construct(bool $durationOnly)
@@ -41,9 +39,8 @@ class TimesheetController extends AbstractController
}
/**
* @Route("/", defaults={"page": 1}, name="timesheet")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated")
* @Method("GET")
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
* @Cache(smaxage="10")
*/
public function indexAction($page, Request $request)
@@ -90,8 +87,7 @@ class TimesheetController extends AbstractController
/**
* The route to stop a running entry.
*
* @Route("/{id}/stop", name="timesheet_stop")
* @Method({"GET"})
* @Route(path="/{id}/stop", name="timesheet_stop", methods={"GET"})
* @Security("is_granted('stop', entry)")
*
* @param Timesheet $entry
@@ -105,8 +101,7 @@ class TimesheetController extends AbstractController
/**
* The route to stop a running entry.
*
* @Route("/start/{id}", name="timesheet_start", requirements={"id" = "\d+"})
* @Method({"GET", "POST"})
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
* @Security("is_granted('start', activity)")
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
@@ -128,8 +123,7 @@ class TimesheetController extends AbstractController
/**
* The route to edit an existing entry.
*
* @Route("/{id}/edit", name="timesheet_edit")
* @Method({"GET", "POST"})
* @Route(path="/{id}/edit", name="timesheet_edit", methods={"GET", "POST"})
* @Security("is_granted('edit', entry)")
*
* @param Timesheet $entry
@@ -138,14 +132,17 @@ class TimesheetController extends AbstractController
*/
public function editAction(Timesheet $entry, Request $request)
{
return $this->edit($entry, $request, 'timesheet_paginated', 'timesheet/edit.html.twig');
if (null !== $request->get('page')) {
return $this->edit($entry, $request, 'timesheet_paginated', 'timesheet/edit.html.twig');
}
return $this->edit($entry, $request, 'timesheet', 'timesheet/edit.html.twig');
}
/**
* The route to create a new entry by form.
*
* @Route("/create", name="timesheet_create")
* @Method({"GET", "POST"})
* @Route(path="/create", name="timesheet_create", methods={"GET", "POST"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
@@ -158,8 +155,7 @@ class TimesheetController extends AbstractController
/**
* The route to delete an existing entry.
*
* @Route("/{id}/delete", name="timesheet_delete")
* @Method({"GET", "POST"})
* @Route(path="/{id}/delete", name="timesheet_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', entry)")
*
* @param Timesheet $entry

View File

@@ -121,6 +121,40 @@ trait TimesheetControllerTrait
$entry->setUser($this->getUser());
$entry->setBegin(new \DateTime());
$start = $request->get('begin');
if ($start !== null) {
$start = \DateTime::createFromFormat('Y-m-d', $start);
if ($start !== false) {
$start->setTime(10, 0, 0); // TODO make me configurable
$entry->setBegin($start);
}
}
$end = $request->get('end');
if ($end !== null) {
$end = \DateTime::createFromFormat('Y-m-d', $end);
if ($end !== false) {
$end->setTime(18, 0, 0); // TODO make me configurable
$entry->setEnd($end);
}
}
$from = $request->get('from');
if ($from !== null) {
$from = new \DateTime($from);
if ($from !== false) {
$entry->setBegin($from);
}
}
$to = $request->get('to');
if ($to !== null) {
$to = new \DateTime($to);
if ($to !== false) {
$entry->setEnd($to);
}
}
$createForm = $this->getCreateForm($entry);
$createForm->handleRequest($request);

View File

@@ -0,0 +1,132 @@
<?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\DataFixtures;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Faker\Factory;
use Faker\Generator;
/**
* Defines the sample data to load in the database when running the unit and
* functional tests or while development.
*
* Execute this command to load the data:
* bin/console doctrine:fixtures:load
*/
class CustomerFixtures extends Fixture
{
public const MIN_CUSTOMERS = 5;
public const MAX_CUSTOMERS = 15;
public const MIN_BUDGET = 0;
public const MAX_BUDGET = 100000;
public const MIN_PROJECTS_PER_CUSTOMER = 10;
public const MAX_PROJECTS_PER_CUSTOMER = 50;
public const MIN_ACTIVITIES_PER_PROJECT = 0;
public const MAX_ACTIVITIES_PER_PROJECT = 15;
/**
* @param ObjectManager $manager
*/
public function load(ObjectManager $manager)
{
$faker = Factory::create();
$amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS);
for ($c = 1; $c <= $amountCustomers; $c++) {
$visibleCustomer = 0 != $c % 3;
$customer = $this->createCustomer($faker, $visibleCustomer);
$projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER);
for ($p = 1; $p <= $projectForCustomer; $p++) {
$visibleProject = 0 != $p % 3;
$project = $this->createProject($faker, $customer, $visibleProject);
$manager->persist($project);
$activityForProject = rand(self::MIN_ACTIVITIES_PER_PROJECT, self::MAX_ACTIVITIES_PER_PROJECT);
for ($a = 1; $a <= $activityForProject; $a++) {
$visibleActivity = 0 != $a % 3;
$activity = $this->createActivity($faker, $project, $visibleActivity);
$manager->persist($activity);
}
}
$manager->persist($customer);
$manager->flush();
$manager->clear();
}
}
/**
* @param Generator $faker
* @param bool $visible
* @return Customer
*/
private function createCustomer(Generator $faker, $visible)
{
$entry = new Customer();
$entry
->setCurrency($faker->currencyCode)
->setName($faker->company . ($visible ? '' : ' (x)'))
->setAddress($faker->address)
->setComment($faker->text)
->setNumber('C-' . $faker->ean8)
->setCountry($faker->countryCode)
->setTimezone($faker->timezone)
->setVisible($visible)
;
return $entry;
}
/**
* @param Generator $faker
* @param Customer $customer
* @param bool $visible
* @return Project
*/
private function createProject(Generator $faker, Customer $customer, $visible)
{
$entry = new Project();
$entry
->setName($faker->catchPhrase . ($visible ? '' : ' (x)'))
->setBudget(rand(self::MIN_BUDGET, self::MAX_BUDGET))
->setComment($faker->text)
->setCustomer($customer)
->setVisible($visible)
;
return $entry;
}
/**
* @param Generator $faker
* @param Project $project
* @param bool $visible
* @return Activity
*/
private function createActivity(Generator $faker, Project $project, $visible)
{
$entry = new Activity();
$entry
->setName($faker->bs . ($visible ? '' : ' (x)'))
->setProject($project)
->setComment($faker->text)
->setVisible($visible)
;
return $entry;
}
}

View File

@@ -1,50 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\DataFixtures;
/**
* Defines reusable sample data.
*/
trait FixturesTrait
{
/**
* @return string[]
*/
protected function getPhrases()
{
return [
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
'Pellentesque vitae velit ex',
'Mauris dapibus risus quis suscipit vulputate',
'Eros diam egestas libero eu vulputate risus',
'In hac habitasse platea dictumst',
'Morbi tempus commodo mattis',
'Ut suscipit posuere justo at vulputate',
'Ut eleifend mauris et risus ultrices egestas',
'Aliquam sodales odio id eleifend tristique',
'Urna nisl sollicitudin id varius orci quam id turpis',
'Nulla porta lobortis ligula vel egestas',
'Curabitur aliquam euismod dolor non ornare',
'Sed varius a risus eget aliquam',
'Nunc viverra elit ac laoreet suscipit',
'Pellentesque et sapien pulvinar consectetur',
];
}
/**
* @return string
*/
protected function getRandomPhrase()
{
$phrases = $this->getPhrases();
return $phrases[array_rand($phrases)];
}
}

View File

@@ -12,6 +12,7 @@ namespace App\DataFixtures;
use App\Entity\InvoiceTemplate;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Faker\Factory;
/**
* Defines the sample data to load in the database when running the unit and
@@ -35,11 +36,13 @@ class InvoiceFixtures extends Fixture
*/
private function loadInvoiceTemplates(ObjectManager $manager)
{
$faker = Factory::create();
$template = new InvoiceTemplate();
$template
->setName('Invoice')
->setTitle('Your company name')
->setCompany('Kimai, Inc.')
->setCompany($faker->company)
->setVat(19)
->setDueDays(14)
->setPaymentTerms(
@@ -49,10 +52,10 @@ class InvoiceFixtures extends Fixture
'as reference.'
)
->setAddress(
'795 Folsom Ave, Suite 600' . PHP_EOL .
'San Francisco, CA 94107' . PHP_EOL .
'Phone: (804) 123-456789' . PHP_EOL .
'Email: email@example.com'
$faker->streetAddress . PHP_EOL .
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode . PHP_EOL .
'Phone: ' . $faker->phoneNumber . PHP_EOL .
'Email: ' . $faker->safeEmail
)
;

View File

@@ -10,37 +10,104 @@
namespace App\DataFixtures;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Entity\UserPreference;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Faker\Factory;
/**
* Defines the sample data to load in the database when running the unit and
* functional tests or while development.
*
* Execute this command to load the data:
* $ php bin/console doctrine:fixtures:load
* bin/console doctrine:fixtures:load
*/
class TimesheetFixtures extends Fixture
class TimesheetFixtures extends Fixture implements DependentFixtureInterface
{
use FixturesTrait;
public const MIN_TIMESHEETS_PER_USER = 50;
public const MAX_TIMESHEETS_PER_USER = 500;
public const MAX_TIMESHEETS_TOTAL = 5000;
public const MIN_RUNNING_TIMESHEETS_PER_USER = 0;
public const MAX_RUNNING_TIMESHEETS_PER_USER = 3;
public const TIMERANGE_DAYS = 1095; // 3 years
public const MIN_MINUTES_PER_ENTRY = 15;
public const MAX_MINUTES_PER_ENTRY = 840; // 14h
public const AMOUNT_TIMESHEET = 5000; // timesheet entries total
public const RATE_MIN = 10; // minimum rate for one hour
public const RATE_MAX = 80; // maximum rate for one hour
public const BATCH_SIZE = 100;
/**
* @return array
*/
public function getDependencies()
{
return [
UserFixtures::class,
CustomerFixtures::class,
];
}
/**
* {@inheritdoc}
*/
public function load(ObjectManager $manager)
{
$this->loadCustomers($manager);
$this->loadProjects($manager);
$this->loadActivities($manager);
$this->loadTimesheet($manager);
$allUser = $this->getAllUsers($manager);
$activities = $this->getAllActivities($manager);
$faker = Factory::create();
// by using array_pop we make sure that at least one activity has NO entry!
array_pop($activities);
foreach ($allUser as $user) {
// random amount of timesheet entries for every user
$timesheetForUser = rand(self::MIN_TIMESHEETS_PER_USER, self::MAX_TIMESHEETS_PER_USER);
for ($i = 1; $i <= $timesheetForUser; $i++) {
if ($i > self::MAX_TIMESHEETS_TOTAL) {
break;
}
$description = null;
if ($i % 3 == 0) {
$description = $faker->text;
} elseif ($i % 7 == 0) {
$description = '';
}
$entry = $this->createTimesheetEntry(
$user,
$activities[array_rand($activities)],
$description
);
$manager->persist($entry);
if ($i % self::BATCH_SIZE == 0) {
//echo '['.$i.'] Timesheets for User ' . $user->getId() . PHP_EOL;
$manager->flush();
$manager->clear(Timesheet::class);
}
}
// create active recordings for test user
$activeEntries = rand(self::MIN_RUNNING_TIMESHEETS_PER_USER, self::MAX_RUNNING_TIMESHEETS_PER_USER);
for ($i = 0; $i < $activeEntries; $i++) {
$entry = $this->createTimesheetEntry(
$user,
$activities[array_rand($activities)],
null,
false
);
$manager->persist($entry);
}
$manager->flush();
$manager->clear(Timesheet::class);
}
$manager->flush();
}
/**
@@ -59,38 +126,6 @@ class TimesheetFixtures extends Fixture
return $all;
}
/**
* @param ObjectManager $manager
* @return Customer[]
*/
protected function getAllCustomers(ObjectManager $manager)
{
$all = [];
/* @var Customer[] $entries */
$entries = $manager->getRepository(Customer::class)->findAll();
foreach ($entries as $temp) {
$all[$temp->getId()] = $temp;
}
return $all;
}
/**
* @param ObjectManager $manager
* @return Project[]
*/
protected function getAllProjects(ObjectManager $manager)
{
$all = [];
/* @var Project[] $entries */
$entries = $manager->getRepository(Project::class)->findAll();
foreach ($entries as $temp) {
$all[$temp->getId()] = $temp;
}
return $all;
}
/**
* @param ObjectManager $manager
* @return Activity[]
@@ -98,7 +133,7 @@ class TimesheetFixtures extends Fixture
protected function getAllActivities(ObjectManager $manager)
{
$all = [];
/* @var Activity[] $entries */
/* @var User[] $entries */
$entries = $manager->getRepository(Activity::class)->findAll();
foreach ($entries as $temp) {
$all[$temp->getId()] = $temp;
@@ -107,338 +142,32 @@ class TimesheetFixtures extends Fixture
return $all;
}
private function loadTimesheet(ObjectManager $manager)
{
$allUser = $this->getAllUsers($manager);
$amountUser = count($allUser);
$allActivity = $this->getAllActivities($manager);
// by using array_pop we make sure that at least one activity has NO entry!
array_pop($allActivity);
for ($i = 0; $i <= self::AMOUNT_TIMESHEET; $i++) {
$entry = $this->createTimesheetEntry(
$allUser[rand(1, $amountUser)],
$allActivity[array_rand($allActivity)],
round($i / 2),
true
);
$manager->persist($entry);
}
// by using array_pop we make sure that at least one user has NO running entry!
array_pop($allUser);
// create active recodinge for test user
foreach ($allUser as $id => $user) {
for ($i = 0; $i < rand(1, 4); $i++) {
$entry = $this->createTimesheetEntry(
$user,
$allActivity[array_rand($allActivity)]
);
$manager->persist($entry);
}
}
$manager->flush();
}
private function createTimesheetEntry(User $user, Activity $activity, $startDay = 0, $setEndDate = false)
private function createTimesheetEntry(User $user, Activity $activity, $description, $setEndDate = true)
{
$start = new \DateTime();
if ($startDay > 0) {
$start = $start->modify('- ' . (rand(1, $startDay)) . ' days');
}
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
$end = clone $start;
$end = $end->modify('+ ' . (rand(1, 43200)) . ' seconds');
//$duration = $end->modify('- ' . $start->getTimestamp() . ' seconds')->getTimestamp();
$duration = $end->getTimestamp() - $start->getTimestamp();
$rate = rand(self::RATE_MIN, self::RATE_MAX);
$entry = new Timesheet();
$entry
->setActivity($activity)
->setDescription($this->getRandomPhrase())
->setDescription($description)
->setUser($user)
->setRate(round(($duration / 3600) * $rate))
->setBegin($start);
if ($setEndDate) {
$end = clone $start;
$end = $end->modify('+ ' . (rand(self::MIN_MINUTES_PER_ENTRY, self::MAX_MINUTES_PER_ENTRY)) . ' minutes');
$duration = $end->getTimestamp() - $start->getTimestamp();
$rate = $user->getPreferenceValue(UserPreference::HOURLY_RATE);
$entry
->setEnd($end)
->setRate(round(($duration / 3600) * $rate))
->setDuration($duration);
}
return $entry;
}
private function loadCustomers(ObjectManager $manager)
{
$allTimezones = \DateTimeZone::listIdentifiers();
$amountTimezone = count($allTimezones);
$allCustomer = $this->getCustomers();
shuffle($allCustomer);
$i = 1;
foreach ($allCustomer as $customerName) {
$visible = 0 != $i++ % 6;
$entry = new Customer();
$entry
->setCurrency($this->getRandomCurrency())
->setName($customerName . ($visible ? '' : '.'))
->setAddress($this->getRandomLocation())
->setComment($this->getRandomPhrase())
->setVisible($visible)
->setNumber('C0815-42-' . $i)
->setCountry('DE') // TODO randomize country ?
->setTimezone($allTimezones[rand(1, $amountTimezone)]);
$manager->persist($entry);
}
$manager->flush();
}
private function loadProjects(ObjectManager $manager)
{
$allCustomer = $this->getAllCustomers($manager);
foreach ($allCustomer as $id => $customer) {
$projectForCustomer = rand(0, 7);
for ($i = 1; $i <= $projectForCustomer; $i++) {
$visible = 0 != $i % 5;
$entry = new Project();
$entry
->setName($this->getRandomProject() . ($visible ? '' : '.'))
->setBudget(rand(500, 100000))
->setComment($this->getRandomPhrase())
->setCustomer($customer)
->setVisible($visible);
$manager->persist($entry);
}
}
$manager->flush();
}
private function loadActivities(ObjectManager $manager)
{
$allProject = $this->getAllProjects($manager);
foreach ($allProject as $projectId => $project) {
$activityCount = rand(0, 10);
for ($i = 1; $i <= $activityCount; $i++) {
$visible = 0 != $i % 4;
$entry = new Activity();
$entry
->setName($this->getRandomActivity() . ($visible ? '' : '.'))
->setProject($project)
->setComment($this->getRandomPhrase())
->setVisible($visible);
$manager->persist($entry);
}
}
$manager->flush();
}
/**
* @return string[]
*/
private function getActivities()
{
return [
'Designing',
'Programming',
'Testing',
'Documentation',
'Pause',
'Research',
'Meeting',
'Hosting',
'Relaunch',
'Support',
'Refactoring',
'Interview',
'Administration',
'DevOps',
'Management',
'Setup',
'Planning',
'Skiing',
'Eating',
'Watching TV',
'Talking',
'Cooking',
'Writing',
'Reading',
'Brainstroming',
'Post Processing',
];
}
/**
* @return string
*/
private function getRandomActivity()
{
$all = $this->getActivities();
return $all[array_rand($all)];
}
/**
* @return string[]
*/
private function getProjects()
{
return [
'User Experience',
'Database Migration',
'Test Automatisation',
'Website Redesign',
'API Development',
'Hosting & Server',
'Customer Relations',
'Infrastructure',
'Princess Cat',
'Software Upgrade',
'Office Management',
'Project X',
'Customer Excellence',
'Crazy Monkey',
'Interface Design',
'Human Ressources',
'Book Release',
'Studio Photography',
'Professional Art',
'Video Production',
];
}
/**
* @return string
*/
private function getRandomProject()
{
$all = $this->getProjects();
return $all[array_rand($all)];
}
/**
* @return string[]
*/
private function getLocations()
{
return [
'Köln',
'München',
'New York',
'Buenos Aires',
'Hawai',
'Amsterdam',
'London',
'San Francisco',
'Tokyo',
'Berlin',
'Sao Paulo',
'Mexico City',
'Moscow',
'Sankt Petersburg',
'Taiwan',
'Perth',
'Sydney',
'Mumbai',
'Lagos',
'Karachi',
'Shanghai',
'Delhi',
];
}
/**
* @return string
*/
private function getRandomLocation()
{
$all = $this->getLocations();
return $all[array_rand($all)];
}
/**
* @return string[]
*/
private function getCustomers()
{
return [
'Acme University',
'Snake Oil',
'Apple',
'Microsoft',
'Google',
'Oracle',
'Yahoo',
'Twitter',
'Zend',
'SensioLabs',
'Samsung',
'Huawai',
'Yandex',
'Baidu',
'Alphabet',
'Amazon.com',
'Berkshire Hathaway',
'Facebook',
'ExxonMobil',
'Nestle',
'Johnson & Johnson',
'Alibaba',
'General Electric',
'Procter & Gamble',
'Wal-Mart Stores',
'Novartis',
'Coca-Cola',
'Wikipedia',
'Walt Disney',
'Merck',
'Pfizer',
"L'Oréal Group",
"McDonald's",
'China Petroleum & Chemical',
'GlaxoSmithKline'
];
}
/**
* @return string[]
*/
private function getCurrencies()
{
return [
'EUR',
'GBP',
'USD',
'RUB',
'JPY',
'CNY',
'INR'
];
}
/**
* @return string
*/
private function getRandomCurrency()
{
$all = $this->getCurrencies();
return $all[array_rand($all)];
}
}

View File

@@ -13,6 +13,7 @@ use App\Entity\User;
use App\Entity\UserPreference;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Faker\Factory;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
@@ -22,16 +23,24 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
* Execute this command to load the data:
* $ php bin/console doctrine:fixtures:load
*/
class AppFixtures extends Fixture
class UserFixtures extends Fixture
{
use FixturesTrait;
public const DEFAULT_PASSWORD = 'kitten';
public const DEFAULT_API_TOKEN = 'api_kitten';
public const DEFAULT_AVATAR = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y';
public const USERNAME_USER = 'john_user';
public const USERNAME_TEAMLEAD = 'tony_teamlead';
public const USERNAME_ADMIN = 'anna_admin';
public const USERNAME_SUPER_ADMIN = 'susan_super';
public const AMOUNT_EXTRA_USER = 2;
public const MIN_RATE = 30;
public const MAX_RATE = 120;
public const BATCH_SIZE = 50;
/**
* @var UserPasswordEncoderInterface
*/
@@ -51,17 +60,21 @@ class AppFixtures extends Fixture
*/
public function load(ObjectManager $manager)
{
$this->loadUsers($manager);
$this->loadDefaultAccounts($manager);
$this->loadTestUsers($manager);
}
/**
* Default users for all test cases
*
* @param ObjectManager $manager
*/
private function loadUsers(ObjectManager $manager)
private function loadDefaultAccounts(ObjectManager $manager)
{
$passwordEncoder = $this->encoder;
foreach ($this->getUserDefinition() as $userData) {
$allUsers = $this->getUserDefinition();
foreach ($allUsers as $userData) {
$user = new User();
$user
->setAlias($userData[0])
@@ -72,18 +85,65 @@ class AppFixtures extends Fixture
->setAvatar($userData[5])
->setEnabled($userData[6])
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
->setApiToken($passwordEncoder->encodePassword($user, self::DEFAULT_API_TOKEN))
->setPreferences([$this->getUserPreference($user)])
;
$preference = new UserPreference();
$preference->setName(UserPreference::HOURLY_RATE);
$preference->setValue(rand(0, 100));
$preference->setUser($user);
$user->setPreferences([$preference]);
$manager->persist($user);
}
$manager->flush();
$manager->clear();
}
/**
* @param User $user
* @return UserPreference
*/
private function getUserPreference(user $user)
{
$preference = new UserPreference();
$preference->setName(UserPreference::HOURLY_RATE);
$preference->setValue(rand(self::MIN_RATE, self::MAX_RATE));
$preference->setUser($user);
return $preference;
}
/**
* Generate randomized test users, which don't have API access.
*
* @param ObjectManager $manager
*/
private function loadTestUsers(ObjectManager $manager)
{
$passwordEncoder = $this->encoder;
$faker = Factory::create();
for ($i = 1; $i <= self::AMOUNT_EXTRA_USER; $i++) {
$user = new User();
$user
->setAlias($faker->name)
->setTitle(substr($faker->jobTitle, 0, 49))
->setUsername($faker->userName)
->setEmail($faker->email)
->setRoles([User::ROLE_USER])
->setAvatar(self::DEFAULT_AVATAR)
->setEnabled(true)
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
->setPreferences([$this->getUserPreference($user)])
;
if ($i % self::BATCH_SIZE == 0) {
$manager->flush();
$manager->clear();
}
$manager->persist($user);
}
$manager->flush();
$manager->clear();
}
/**
@@ -93,29 +153,29 @@ class AppFixtures extends Fixture
{
return [
[
'Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', 'ROLE_CUSTOMER',
'Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', User::ROLE_CUSTOMER,
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y', true
],
[
'John Doe', 'Developer', self::USERNAME_USER, 'john_user@example.com', 'ROLE_USER',
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', true
'John Doe', 'Developer', self::USERNAME_USER, 'john_user@example.com', User::ROLE_USER,
self::DEFAULT_AVATAR, true
],
// inactive user to test login
[
'Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', 'ROLE_USER',
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', false
'Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', User::ROLE_USER,
self::DEFAULT_AVATAR, false
],
[
'Tony Maier', 'Head of Sales', self::USERNAME_TEAMLEAD, 'tony_teamlead@example.com', 'ROLE_TEAMLEAD',
'Tony Maier', 'Head of Sales', self::USERNAME_TEAMLEAD, 'tony_teamlead@example.com', User::ROLE_TEAMLEAD,
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', true
],
// no avatar to test default image macro
[
'Anna Smith', 'Administrator', self::USERNAME_ADMIN, 'anna_admin@example.com', 'ROLE_ADMIN', null, true
'Anna Smith', 'Administrator', self::USERNAME_ADMIN, 'anna_admin@example.com', User::ROLE_ADMIN, null, true
],
// no alias to test twig username macro
[
null, 'Super Administrator', self::USERNAME_SUPER_ADMIN, 'susan_super@example.com', 'ROLE_SUPER_ADMIN',
null, 'Super Administrator', self::USERNAME_SUPER_ADMIN, 'susan_super@example.com', User::ROLE_SUPER_ADMIN,
'/build/images/default_avatar.png', true
]
];

View File

@@ -16,12 +16,13 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* This is the class that loads and manages your bundle configuration
* This class that loads and manages the Kimai configuration and container parameter.
*/
class AppExtension extends Extension implements PrependExtensionInterface
{
/**
* {@inheritdoc}
* @param array $configs
* @param ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container)
{
@@ -34,11 +35,41 @@ class AppExtension extends Extension implements PrependExtensionInterface
}
$container->setParameter('kimai.languages', $config['languages']);
$container->setParameter('kimai.calendar', $config['calendar']);
$container->setParameter('kimai.theme', $config['theme']);
$container->setParameter('kimai.dashboard', $config['dashboard']);
$container->setParameter('kimai.widgets', $config['widgets']);
$this->createUserParameter($config, $container);
$this->createTimesheetParameter($config, $container);
$this->createInvoiceParameter($config, $container);
}
/**
* @param array $config
* @param ContainerBuilder $container
*/
public function createUserParameter(array $config, ContainerBuilder $container)
{
if (!$config['user']['registration']) {
$routes = $container->getParameter('admin_lte_theme.routes');
$routes['adminlte_registration'] = null;
$container->setParameter('admin_lte_theme.routes', $routes);
}
if (!$config['user']['password_reset']) {
$routes = $container->getParameter('admin_lte_theme.routes');
$routes['adminlte_password_reset'] = null;
$container->setParameter('admin_lte_theme.routes', $routes);
}
$container->setParameter('kimai.fosuser', $config['user']);
}
/**
* @param array $config
* @param ContainerBuilder $container
*/
public function createTimesheetParameter(array $config, ContainerBuilder $container)
{
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
@@ -70,6 +101,7 @@ class AppExtension extends Extension implements PrependExtensionInterface
*/
public function prepend(ContainerBuilder $container)
{
/*
$configuration = new Configuration();
$configs = $container->getExtensionConfig($this->getAlias());
try {
@@ -87,10 +119,11 @@ class AppExtension extends Extension implements PrependExtensionInterface
],
]
);
*/
}
/**
* {@inheritdoc}
* @return string
*/
public function getAlias()
{

View File

@@ -0,0 +1,33 @@
<?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\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Dynamically adds twig globals.
*/
class TwigContextCompilerPass implements CompilerPassInterface
{
/**
* @param ContainerBuilder $container
* @throws \Exception
*/
public function process(ContainerBuilder $container)
{
$twig = $container->getDefinition('twig');
$theme = $container->getParameter('kimai.theme');
$durationOnly = $container->getParameter('kimai.timesheet.duration_only');
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
}
}

View File

@@ -9,11 +9,15 @@
namespace App\DependencyInjection;
use App\Model\DashboardSection;
use App\Model\Widget;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files
* This class validates and merges configuration from the files:
* - config/packages/kimai.yaml
* - config/packages/local.yaml
*/
class Configuration implements ConfigurationInterface
{
@@ -27,100 +31,284 @@ class Configuration implements ConfigurationInterface
$rootNode
->children()
->arrayNode('timesheet')
->children()
->booleanNode('duration_only')
->defaultValue(false)
->end()
->arrayNode('rounding')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->children()
->arrayNode('days')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([])
->end()
->integerNode('begin')
->defaultValue(0)
->end()
->integerNode('end')
->defaultValue(0)
->end()
->integerNode('duration')
->defaultValue(0)
->end()
->end()
->end()
->defaultValue([])
->end()
->arrayNode('rates')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->children()
->arrayNode('days')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([])
->end()
->floatNode('factor')
->isRequired()
->defaultValue(1)
->end()
->end()
->end()
->defaultValue([])
->end()
->end()
->end()
->arrayNode('invoice')
->children()
->arrayNode('renderer')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([
'default' => 'App\Controller\InvoiceController::invoiceAction',
])
->end()
->arrayNode('calculator')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([
'default' => 'App\Invoice\DefaultCalculator',
])
->end()
->arrayNode('number_generator')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([
'default' => 'App\Invoice\DateNumberGenerator',
])
->end()
->end()
->end()
->arrayNode('languages')
->arrayPrototype()
->children()
->scalarNode('date_short')->end()
->end()
->end()
->end()
->append($this->getUserNode())
->append($this->getTimesheetNode())
->append($this->getInvoiceNode())
->append($this->getLanguagesNode())
->append($this->getCalendarNode())
->append($this->getThemeNode())
->append($this->getDashboardNode())
->append($this->getWidgetsNode())
->end()
->end();
return $treeBuilder;
}
protected function getTimesheetNode()
{
$builder = new TreeBuilder();
$node = $builder->root('timesheet');
$node
->children()
->booleanNode('duration_only')
->defaultValue(false)
->end()
->arrayNode('rounding')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->children()
->arrayNode('days')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([])
->end()
->integerNode('begin')
->defaultValue(0)
->end()
->integerNode('end')
->defaultValue(0)
->end()
->integerNode('duration')
->defaultValue(0)
->end()
->end()
->end()
->defaultValue([])
->end()
->arrayNode('rates')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->children()
->arrayNode('days')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([])
->end()
->floatNode('factor')
->isRequired()
->defaultValue(1)
->end()
->end()
->end()
->defaultValue([])
->end()
->end()
;
return $node;
}
protected function getInvoiceNode()
{
$builder = new TreeBuilder();
$node = $builder->root('invoice');
$node
->children()
->arrayNode('renderer')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([
'default' => 'App\Controller\InvoiceController::invoiceAction',
])
->end()
->arrayNode('calculator')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([
'default' => 'App\Invoice\DefaultCalculator',
])
->end()
->arrayNode('number_generator')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->isRequired()
->prototype('scalar')->end()
->defaultValue([
'default' => 'App\Invoice\DateNumberGenerator',
])
->end()
->end()
;
return $node;
}
protected function getLanguagesNode()
{
$builder = new TreeBuilder();
$node = $builder->root('languages');
$node
->arrayPrototype()
->children()
->scalarNode('date_short')->end()
->scalarNode('duration')->end()
->scalarNode('duration_short')->end()
->end()
->end()
;
return $node;
}
protected function getCalendarNode()
{
$builder = new TreeBuilder();
$node = $builder->root('calendar');
$node
->children()
->booleanNode('week_numbers')->defaultTrue()->end()
->integerNode('day_limit')->defaultValue(4)->end()
->arrayNode('businessHours')
->addDefaultsIfNotSet()
->children()
->arrayNode('days')
->requiresAtLeastOneElement()
->prototype('integer')->end()
->defaultValue([1, 2, 3, 4, 5])
->end()
->scalarNode('begin')->defaultValue('08:00')->end()
->scalarNode('end')->defaultValue('20:00')->end()
->end()
->end()
->arrayNode('google')
->addDefaultsIfNotSet()
->children()
->scalarNode('api_key')->defaultNull()->end()
->arrayNode('sources')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->children()
->scalarNode('id')->isRequired()->end()
->scalarNode('color')->defaultValue('#ccc')->end()
->end()
->end()
->end()
->end()
->end()
->end()
;
return $node;
}
protected function getThemeNode()
{
$builder = new TreeBuilder();
$node = $builder->root('theme');
$node
->addDefaultsIfNotSet()
->children()
->integerNode('active_warning')
->defaultValue(3)
->end()
->scalarNode('box_color')
->defaultValue('green')
->end()
->end()
;
return $node;
}
protected function getUserNode()
{
$builder = new TreeBuilder();
$node = $builder->root('user');
$node
->addDefaultsIfNotSet()
->children()
->booleanNode('registration')
->defaultTrue()
->end()
->booleanNode('password_reset')
->defaultTrue()
->end()
->end()
;
return $node;
}
protected function getWidgetsNode()
{
$builder = new TreeBuilder();
$node = $builder->root('widgets');
$node
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->addDefaultsIfNotSet()
->children()
->scalarNode('title')->isRequired()->end()
->scalarNode('query')->isRequired()->end()
->booleanNode('user')->defaultFalse()->end()
->scalarNode('begin')->end()
->scalarNode('end')->end()
->scalarNode('icon')->defaultValue('')->end()
->scalarNode('color')->defaultValue('')->end()
->scalarNode('type')
->validate()
->ifNotInArray([Widget::TYPE_COUNTER, Widget::TYPE_MORE])->thenInvalid('Unknown widget type')
->end()
->defaultValue(Widget::TYPE_COUNTER)
->end()
->end()
->end()
;
return $node;
}
protected function getDashboardNode()
{
$builder = new TreeBuilder();
$node = $builder->root('dashboard');
$node
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
->arrayPrototype()
->addDefaultsIfNotSet()
->children()
->scalarNode('type')
->validate()
->ifNotInArray([DashboardSection::TYPE_SIMPLE, DashboardSection::TYPE_CHART])->thenInvalid('Unknown section type')
->end()
->defaultValue(DashboardSection::TYPE_SIMPLE)
->end()
->integerNode('order')->defaultValue(0)->end()
->scalarNode('title')->end()
->scalarNode('permission')->isRequired()->end()
->arrayNode('widgets')
->isRequired()
->performNoDeepMerging()
->scalarPrototype()
->end()
->end()
->end()
->end()
;
return $node;
}
}

View File

@@ -11,6 +11,7 @@ namespace App\Doctrine;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Events;
/**
* Adds a prefix to every doctrine entity AKA database table
@@ -19,16 +20,27 @@ class TablePrefixSubscriber implements EventSubscriber
{
protected $prefix = '';
/**
* @param string $prefix
*/
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
/**
* @return array|string[]
*/
public function getSubscribedEvents()
{
return ['loadClassMetadata'];
return [
Events::loadClassMetadata,
];
}
/**
* @param LoadClassMetadataEventArgs $args
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();

View File

@@ -69,6 +69,14 @@ class Activity
*/
private $timesheets;
/**
* @return Timesheet[]
*/
public function getTimesheets(): array
{
return $this->timesheets;
}
/**
* @return Project
*/

View File

@@ -64,7 +64,7 @@ class Timesheet
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(name="user", referencedColumnName="id")
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE")
* @Assert\NotNull()
*/
private $user;

View File

@@ -78,6 +78,18 @@ class User extends BaseUser implements UserInterface
*/
private $avatar;
/**
* @var string
*
* @ORM\Column(name="api_token", type="string", length=255, nullable=true)
*/
protected $apiToken;
/**
* @var string
*/
protected $plainApiToken;
/**
* @var UserPreference[]|Collection
*
@@ -179,6 +191,44 @@ class User extends BaseUser implements UserInterface
return $this;
}
/**
* @return string
*/
public function getApiToken()
{
return $this->apiToken;
}
/**
* @param string $apiToken
* @return User
*/
public function setApiToken($apiToken)
{
$this->apiToken = $apiToken;
return $this;
}
/**
* @return string
*/
public function getPlainApiToken(): ?string
{
return $this->plainApiToken;
}
/**
* @param string $plainApiToken
* @return User
*/
public function setPlainApiToken(string $plainApiToken)
{
$this->plainApiToken = $plainApiToken;
return $this;
}
/**
* @return UserPreference[]|Collection
*/
@@ -191,12 +241,13 @@ class User extends BaseUser implements UserInterface
* @param UserPreference[]|Collection<UserPreference> $preferences
* @return User
*/
public function setPreferences(array $preferences)
public function setPreferences($preferences)
{
if (!($preferences instanceof Collection) && is_array($preferences)) {
$preferences = new ArrayCollection($preferences);
$this->preferences = new ArrayCollection();
foreach ($preferences as $preference) {
$this->addPreference($preference);
}
$this->preferences = $preferences;
return $this;
}
@@ -238,6 +289,7 @@ class User extends BaseUser implements UserInterface
public function addPreference(UserPreference $preference)
{
$this->preferences->add($preference);
$preference->setUser($this);
return $this;
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Event;
use App\Entity\User;
use App\Model\DashboardSection;
use Symfony\Component\EventDispatcher\Event;
class DashboardEvent extends Event
{
public const DASHBOARD = 'app.dashboard';
/**
* @var User
*/
protected $user;
/**
* @var DashboardSection[]
*/
protected $widgetRows = [];
/**
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @param DashboardSection $row
* @return DashboardEvent
*/
public function addSection(DashboardSection $row)
{
$this->widgetRows[] = $row;
return $this;
}
/**
* @return DashboardSection[]
*/
public function getSections()
{
return $this->widgetRows;
}
}

View File

@@ -0,0 +1,139 @@
<?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\EventSubscriber;
use App\Entity\User;
use App\Event\DashboardEvent;
use App\Model\DashboardSection;
use App\Model\Widget;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\UserRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Used to add Dashboard widgets for users with ROLE_ADMIN.
*/
class DashboardSubscriber implements EventSubscriberInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $security;
/**
* @var UserRepository
*/
protected $user;
/**
* @var ActivityRepository
*/
protected $activity;
/**
* @var ProjectRepository
*/
protected $project;
/**
* @var CustomerRepository
*/
protected $customer;
/**
* @param AuthorizationCheckerInterface $security
* @param UserRepository $user
* @param ActivityRepository $activity
* @param ProjectRepository $project
* @param CustomerRepository $customer
*/
public function __construct(
AuthorizationCheckerInterface $security,
UserRepository $user,
ActivityRepository $activity,
ProjectRepository $project,
CustomerRepository $customer
) {
$this->security = $security;
$this->user = $user;
$this->activity = $activity;
$this->project = $project;
$this->customer = $customer;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
DashboardEvent::DASHBOARD => ['onDashboardEvent', 100],
];
}
/**
* @param DashboardEvent $event
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function onDashboardEvent(DashboardEvent $event)
{
if (!$this->security->isGranted(User::ROLE_ADMIN)) {
return;
}
$this->addAdminWidgets($event);
}
/**
* @param DashboardEvent $event
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function addAdminWidgets(DashboardEvent $event)
{
$section = new DashboardSection('dashboard.admin');
$section->setOrder(100);
$widget = new Widget('stats.userTotal', $this->user->countUser());
$widget
->setRoute('admin_user')
->setIcon('user')
->setType(Widget::TYPE_MORE)
;
$section->addWidget($widget);
$widget = new Widget('stats.customerTotal', $this->customer->countCustomer());
$widget
->setRoute('admin_customer')
->setIcon('customer')
->setColor('blue')
->setType(Widget::TYPE_MORE)
;
$section->addWidget($widget);
$widget = new Widget('stats.projectTotal', $this->project->countProject());
$widget
->setRoute('admin_project')
->setIcon('project')
->setColor('yellow')
->setType(Widget::TYPE_MORE)
;
$section->addWidget($widget);
$widget = new Widget('stats.activityTotal', $this->activity->countActivity());
$widget
->setRoute('admin_activity')
->setIcon('activity')
->setColor('purple')
->setType(Widget::TYPE_MORE)
;
$section->addWidget($widget);
$event->addSection($section);
}
}

View File

@@ -35,7 +35,7 @@ class DocumentationLinkExtension extends AbstractTypeExtension
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['documentation'] = $options['documentation'] ?? '';
$view->vars['docu_chapter'] = $options['docu_chapter'] ?? null;
}
/**
@@ -43,7 +43,8 @@ class DocumentationLinkExtension extends AbstractTypeExtension
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefined(['documentation']);
$resolver->setDefaults(['documentation' => null]);
$resolver->setDefined(['docu_chapter']);
$resolver->setAllowedTypes('docu_chapter', 'string');
$resolver->setDefault('docu_chapter', '');
}
}

View File

@@ -93,7 +93,7 @@ class TimesheetEditForm extends AbstractType
'csrf_token_id' => 'timesheet_edit',
'duration_only' => false,
'include_user' => false,
'documentation' => 'timesheet',
'docu_chapter' => 'timesheet',
]);
}
}

View File

@@ -43,7 +43,7 @@ class ActivityType extends AbstractType
{
$resolver->setDefaults([
'label' => 'label.activity',
'class' => 'Kimai:Activity',
'class' => Activity::class,
'choice_label' => [$this, 'choiceLabel'],
'group_by' => [$this, 'groupBy'],
'query_builder' => function (ActivityRepository $repo) {

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Entity\Customer;
use App\Repository\CustomerRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
@@ -26,7 +27,7 @@ class CustomerType extends AbstractType
{
$resolver->setDefaults([
'label' => 'label.customer',
'class' => 'Kimai:Customer',
'class' => Customer::class,
'choice_label' => 'name',
'query_builder' => function (CustomerRepository $repo) {
return $repo->builderForEntityType(null);

View File

@@ -67,7 +67,7 @@ class DurationType extends AbstractType
$builder->addModelTransformer(new CallbackTransformer(
function ($intToFormat) use ($formatter) {
try {
return $formatter->format($intToFormat, true);
return $formatter->format($intToFormat);
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage());
}

View 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\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to set the users API token.
*/
class UserApiTokenType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('plainApiToken', RepeatedType::class, [
'type' => PasswordType::class,
'first_options' => ['label' => 'label.api_token'],
'second_options' => ['label' => 'label.api_token_repeat'],
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'edit_user_api_token',
]);
}
}

View File

@@ -17,7 +17,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to create and manipulate Users.
* Defines the form used to set the users password.
*/
class UserPasswordType extends AbstractType
{

View File

@@ -11,6 +11,7 @@ namespace App;
use App\DependencyInjection\AppExtension;
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
use App\Timesheet\CalculatorInterface;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
@@ -66,17 +67,48 @@ class Kernel extends BaseKernel
$loader->load($confDir . '/services_' . $this->environment . self::CONFIG_EXTS, 'glob');
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
}
protected function configureRoutes(RouteCollectionBuilder $routes)
{
$confDir = $this->getProjectDir() . '/config';
// some routes are based on app configs and will be imported manually
$this->configureFosUserRoutes($routes);
// load bundle specific route files
if (is_dir($confDir . '/routes/')) {
$routes->import($confDir . '/routes/*' . self::CONFIG_EXTS, '/', 'glob');
}
// load environment specific route files
if (is_dir($confDir . '/routes/' . $this->environment)) {
$routes->import($confDir . '/routes/' . $this->environment . '/**/*' . self::CONFIG_EXTS, '/', 'glob');
}
// load application routes
$routes->import($confDir . '/routes' . self::CONFIG_EXTS, '/', 'glob');
}
protected function configureFosUserRoutes(RouteCollectionBuilder $routes)
{
$features = $this->getContainer()->getParameter('kimai.fosuser');
// Expose the user registration feature
if ($features['registration']) {
$routes->import(
'@FOSUserBundle/Resources/config/routing/registration.xml',
'/{_locale}/register'
);
}
// Expose the users password-reset feature
if ($features['password_reset']) {
$routes->import(
'@FOSUserBundle/Resources/config/routing/resetting.xml',
'/{_locale}/resetting'
);
}
}
}

View File

@@ -16,7 +16,7 @@ use Doctrine\DBAL\Schema\Schema;
/**
* Initial database structure of Kimai 2.
* This file is only required for testing the migrations!
* This file is mainly required for testing the migrations.
*/
final class Version20180701120000 extends AbstractMigration
{

View File

@@ -0,0 +1,94 @@
<?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\Index;
use Doctrine\DBAL\Schema\Schema;
/**
* Migrations fot the "delete user" feature.
*
* Adds constraints to the timesheet table, so all timesheet entries. will be deleted when a user is deleted.
*/
final class Version20180730044139 extends AbstractMigration
{
/**
* @var Index[]
*/
protected $indexesOld = [];
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
*/
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$timesheet = $this->getTableName('timesheet');
$user = $this->getTableName('users');
$activity = $this->getTableName('activities');
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activity . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE');
}
}
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
*/
public function down(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$timesheet = $this->getTableName('timesheet');
$user = $this->getTableName('user');
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)');
}
}
}

View File

@@ -0,0 +1,79 @@
<?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;
/**
* Added "API-token" to users table.
*/
final class Version20180805183527 extends AbstractMigration
{
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
*/
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$user = $this->getTableName('users');
if ($platform === 'sqlite') {
$this->addSql('ALTER TABLE ' . $user . ' ADD COLUMN api_token VARCHAR(255) DEFAULT NULL');
} else {
$this->addSql('ALTER TABLE ' . $user . ' ADD api_token VARCHAR(255) DEFAULT NULL');
}
}
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
*/
public function down(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$user = $this->getTableName('user');
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX UNIQ_B9AC5BCE92FC23A8');
$this->addSql('DROP INDEX UNIQ_B9AC5BCEA0D96FBF');
$this->addSql('DROP INDEX UNIQ_B9AC5BCEC05FB297');
$this->addSql('DROP INDEX UNIQ_B9AC5BCEF85E0677');
$this->addSql('DROP INDEX UNIQ_B9AC5BCEE7927C74');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $user . ' AS SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar FROM ' . $user);
$this->addSql('DROP TABLE ' . $user);
$this->addSql('CREATE TABLE ' . $user . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, username_canonical VARCHAR(180) NOT NULL, email VARCHAR(180) NOT NULL, email_canonical VARCHAR(180) NOT NULL, enabled BOOLEAN NOT NULL, salt VARCHAR(255) DEFAULT NULL, password VARCHAR(255) NOT NULL, last_login DATETIME DEFAULT NULL, confirmation_token VARCHAR(180) DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, roles CLOB NOT NULL, alias VARCHAR(60) DEFAULT NULL, registration_date DATETIME DEFAULT NULL, title VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL)');
$this->addSql('INSERT INTO ' . $user . ' (id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar) SELECT id, username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar FROM __temp__' . $user);
$this->addSql('DROP TABLE __temp__' . $user);
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCE92FC23A8 ON ' . $user . ' (username_canonical)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEA0D96FBF ON ' . $user . ' (email_canonical)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEC05FB297 ON ' . $user . ' (confirmation_token)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEF85E0677 ON ' . $user . ' (username)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B9AC5BCEE7927C74 ON ' . $user . ' (email)');
} else {
$this->addSql('ALTER TABLE ' . $user . ' DROP api_token');
}
}
}

View File

@@ -0,0 +1,106 @@
<?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\Model;
class DashboardSection
{
public const TYPE_SIMPLE = 'simple';
public const TYPE_CHART = 'chart';
/**
* @var null|string
*/
protected $title;
/**
* @var Widget[]
*/
protected $widgets = [];
/**
* @var int
*/
protected $order = 0;
/**
* @var string
*/
protected $type = self::TYPE_SIMPLE;
/**
* @param null|string $title
*/
public function __construct(?string $title)
{
$this->title = $title;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @param string $type
* @return DashboardSection
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* @return int
*/
public function getOrder(): int
{
return $this->order;
}
/**
* @param int $order
* @return DashboardSection
*/
public function setOrder(int $order)
{
$this->order = $order;
return $this;
}
/**
* @return string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @return Widget[]
*/
public function getWidgets(): array
{
return $this->widgets;
}
/**
* @param Widget $widget
* @return DashboardSection
*/
public function addWidget(Widget $widget)
{
$this->widgets[] = $widget;
return $this;
}
}

View File

@@ -1,77 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
/**
* Timesheet statistics for all user.
*/
class TimesheetGlobalStatistic extends TimesheetStatistic
{
/**
* @var int
*/
protected $activeThisMonth = 0;
/**
* @var int
*/
protected $activeTotal = 0;
/**
* @var int
*/
protected $activeCurrently = 0;
/**
* @return int
*/
public function getActiveCurrently()
{
return $this->activeCurrently;
}
/**
* @param int $activeCurrently
*/
public function setActiveCurrently($activeCurrently)
{
$this->activeCurrently = $activeCurrently;
}
/**
* @return int
*/
public function getActiveThisMonth()
{
return $this->activeThisMonth;
}
/**
* @param int $activeThisMonth
*/
public function setActiveThisMonth($activeThisMonth)
{
$this->activeThisMonth = $activeThisMonth;
}
/**
* @return int
*/
public function getActiveTotal()
{
return $this->activeTotal;
}
/**
* @param int $activeTotal
*/
public function setActiveTotal($activeTotal)
{
$this->activeTotal = $activeTotal;
}
}

View File

@@ -36,6 +36,10 @@ class TimesheetStatistic
* @var \DateTime
*/
protected $firstEntry;
/**
* @var int
*/
protected $recordsTotal = 0;
/**
* @return int
@@ -50,7 +54,7 @@ class TimesheetStatistic
*/
public function setDurationThisMonth($durationThisMonth)
{
$this->durationThisMonth = $durationThisMonth;
$this->durationThisMonth = (int) $durationThisMonth;
}
/**
@@ -66,7 +70,7 @@ class TimesheetStatistic
*/
public function setAmountTotal($amountTotal)
{
$this->amountTotal = $amountTotal;
$this->amountTotal = (int) $amountTotal;
}
/**
@@ -82,7 +86,7 @@ class TimesheetStatistic
*/
public function setDurationTotal($durationTotal)
{
$this->durationTotal = $durationTotal;
$this->durationTotal = (int) $durationTotal;
}
/**
@@ -98,7 +102,7 @@ class TimesheetStatistic
*/
public function setAmountThisMonth($amountThisMonth)
{
$this->amountThisMonth = $amountThisMonth;
$this->amountThisMonth = (int) $amountThisMonth;
}
/**
@@ -116,4 +120,23 @@ class TimesheetStatistic
{
$this->firstEntry = $firstEntry;
}
/**
* @return int
*/
public function getRecordsTotal(): int
{
return $this->recordsTotal;
}
/**
* @param int $recordsTotal
* @return TimesheetStatistic
*/
public function setRecordsTotal(int $recordsTotal)
{
$this->recordsTotal = $recordsTotal;
return $this;
}
}

View File

@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Model;
/**
* User statistics
*/
class UserStatistic
{
/**
* @var int
*/
protected $totalAmount = 0;
/**
* @return int
*/
public function getTotalAmount()
{
return $this->totalAmount;
}
/**
* @param int $totalAmount
*/
public function setTotalAmount($totalAmount)
{
$this->totalAmount = $totalAmount;
}
}

219
src/Model/Widget.php Normal file
View File

@@ -0,0 +1,219 @@
<?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\Model;
class Widget
{
public const TYPE_COUNTER = 'counter';
public const TYPE_MORE = 'more';
public const DATA_TYPE_INT = 'int';
public const DATA_TYPE_MONEY = 'money';
public const DATA_TYPE_DURATION = 'duration';
/**
* @var string|null
*/
protected $title;
/**
* @var string
*/
protected $icon = '';
/**
* @var string
*/
protected $color = '';
/**
* @var string
*/
protected $type = self::TYPE_COUNTER;
/**
* @var string
*/
protected $range;
/**
* @var string|null
*/
protected $route;
/**
* @var array
*/
protected $routeOptions = [];
/**
* @var mixed
*/
protected $data;
/**
* @var string
*/
protected $dataType = self::DATA_TYPE_INT;
/**
* @param string $title
* @param mixed $data
*/
public function __construct(string $title, $data)
{
$this->title = $title;
$this->data = $data;
}
/**
* @return string
*/
public function getDataType(): string
{
return $this->dataType;
}
/**
* @param string $dataType
* @return Widget
*/
public function setDataType(string $dataType)
{
$this->dataType = $dataType;
return $this;
}
/**
* @return array
*/
public function getRouteOptions(): array
{
return $this->routeOptions;
}
/**
* @param array $routeOptions
* @return Widget
*/
public function setRouteOptions(array $routeOptions)
{
$this->routeOptions = $routeOptions;
return $this;
}
/**
* @return string|null
*/
public function getRoute(): ?string
{
return $this->route;
}
/**
* @param string $route
* @return Widget
*/
public function setRoute(string $route)
{
$this->route = $route;
return $this;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* @param mixed $data
* @return Widget
*/
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* @return string|null
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param string $title
* @return Widget
*/
public function setTitle(string $title)
{
$this->title = $title;
return $this;
}
/**
* @return string
*/
public function getIcon(): string
{
return $this->icon;
}
/**
* @param string $icon
* @return Widget
*/
public function setIcon(string $icon)
{
$this->icon = $icon;
return $this;
}
/**
* @return string
*/
public function getColor(): string
{
return $this->color;
}
/**
* @param string $color
* @return Widget
*/
public function setColor(string $color)
{
$this->color = $color;
return $this;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @param string $type
* @return Widget
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
}

View File

@@ -15,6 +15,8 @@ use App\Entity\User;
use App\Model\ActivityStatistic;
use App\Repository\Query\ActivityQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
/**
* Class ActivityRepository
@@ -76,21 +78,11 @@ class ActivityRepository extends AbstractRepository
}
/**
* Return global statistic data for all user.
*
* @return ActivityStatistic
* @throws \Doctrine\ORM\NonUniqueResultException
* @return int
*/
public function getGlobalStatistics()
public function countActivity()
{
$countAll = $this->getEntityManager()
->createQuery('SELECT COUNT(a.id) FROM ' . Activity::class . ' a')
->getSingleScalarResult();
$stats = new ActivityStatistic();
$stats->setCount($countAll);
return $stats;
return $this->count([]);
}
/**
@@ -140,7 +132,7 @@ class ActivityRepository extends AbstractRepository
/**
* @param ActivityQuery $query
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(ActivityQuery $query)
{

View File

@@ -16,6 +16,8 @@ use App\Entity\Timesheet;
use App\Model\CustomerStatistic;
use App\Repository\Query\CustomerQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
/**
* Class CustomerRepository
@@ -32,21 +34,11 @@ class CustomerRepository extends AbstractRepository
}
/**
* Return statistic data for all customer.
*
* @return CustomerStatistic
* @throws \Doctrine\ORM\NonUniqueResultException
* @return int
*/
public function getGlobalStatistics()
public function countCustomer()
{
$countAll = $this->getEntityManager()
->createQuery('SELECT COUNT(c.id) FROM ' . Customer::class . ' c')
->getSingleScalarResult();
$stats = new CustomerStatistic();
$stats->setCount($countAll);
return $stats;
return $this->count([]);
}
/**
@@ -107,7 +99,7 @@ class CustomerRepository extends AbstractRepository
/**
* @param CustomerQuery $query
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(CustomerQuery $query)
{

View File

@@ -12,6 +12,8 @@ namespace App\Repository;
use App\Entity\InvoiceTemplate;
use App\Repository\Query\BaseQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
/**
* Class InvoiceTemplateRepository
@@ -40,7 +42,7 @@ class InvoiceTemplateRepository extends AbstractRepository
/**
* @param BaseQuery $query
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(BaseQuery $query)
{

View File

@@ -15,6 +15,8 @@ use App\Entity\Timesheet;
use App\Model\ProjectStatistic;
use App\Repository\Query\ProjectQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
/**
* Class ProjectRepository
@@ -31,21 +33,11 @@ class ProjectRepository extends AbstractRepository
}
/**
* Return statistic data for all user.
*
* @return ProjectStatistic
* @throws \Doctrine\ORM\NonUniqueResultException
* @return int
*/
public function getGlobalStatistics()
public function countProject()
{
$countAll = $this->getEntityManager()
->createQuery('SELECT COUNT(p.id) FROM ' . Project::class . ' p')
->getSingleScalarResult();
$stats = new ProjectStatistic();
$stats->setCount($countAll);
return $stats;
return $this->count([]);
}
/**
@@ -100,7 +92,7 @@ class ProjectRepository extends AbstractRepository
/**
* @param ProjectQuery $query
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(ProjectQuery $query)
{

View File

@@ -20,6 +20,7 @@ class BaseQuery
public const DEFAULT_PAGESIZE = 25;
public const DEFAULT_PAGE = 1;
public const RESULT_TYPE_OBJECTS = 'Objects';
public const RESULT_TYPE_PAGER = 'PagerFanta';
public const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder';
@@ -141,13 +142,18 @@ class BaseQuery
/**
* @param string $resultType
* @return $this
* @throws \InvalidArgumentException
*/
public function setResultType($resultType)
public function setResultType(string $resultType)
{
if (in_array($resultType, [self::RESULT_TYPE_PAGER, self::RESULT_TYPE_QUERYBUILDER])) {
$this->resultType = $resultType;
$allowed = [self::RESULT_TYPE_PAGER, self::RESULT_TYPE_QUERYBUILDER, self::RESULT_TYPE_OBJECTS];
if (!in_array($resultType, $allowed)) {
throw new \InvalidArgumentException('Unsupported query result type');
}
$this->resultType = $resultType;
return $this;
}

Some files were not shown because too many files have changed in this diff Show More