Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fb6a16246 | ||
|
|
3af8070a57 | ||
|
|
ebe007eb78 | ||
|
|
55b7e192aa | ||
|
|
62f91f4d38 | ||
|
|
b1546c0568 | ||
|
|
af2d1c3a6e | ||
|
|
ca893b303a | ||
|
|
142369d651 | ||
|
|
3241c73e7c | ||
|
|
d6d9abd05e | ||
|
|
85ff2ff9de | ||
|
|
dbbb434723 | ||
|
|
7804ef83ce | ||
|
|
fb2d898e4c | ||
|
|
c6e99b35d5 | ||
|
|
3dcd38eca7 | ||
|
|
39ea44fdc3 | ||
|
|
2f84f3751a | ||
|
|
a7780dac9e | ||
|
|
e549a0824f | ||
|
|
8b8583d48b | ||
|
|
a8a8424ced | ||
|
|
0996eca3c5 | ||
|
|
e6f8bc8ae2 | ||
|
|
5ccc1c991b | ||
|
|
1dd5f2b72f |
@@ -1,2 +1,2 @@
|
||||
unreleased=true
|
||||
future-release=0.3
|
||||
future-release=0.4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,6 +31,7 @@
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -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))
|
||||
|
||||
15
README.md
15
README.md
@@ -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).
|
||||
|
||||
|
||||
45
UPGRADING.md
45
UPGRADING.md
@@ -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:**
|
||||
|
||||
@@ -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
7
assets/css/calendar.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.popover-content {
|
||||
.calendar-entry {
|
||||
ul {
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
assets/css/content.scss
Normal file
30
assets/css/content.scss
Normal 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
16
assets/css/dashboard.scss
Normal 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
36
assets/css/invoice.scss
Normal 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
46
assets/css/navbar.scss
Normal 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
29
assets/css/toolbar.scss
Normal 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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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
2454
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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],
|
||||
];
|
||||
|
||||
7
config/packages/dev/jms_serializer.yaml
Normal file
7
config/packages/dev/jms_serializer.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json:
|
||||
options:
|
||||
- JSON_PRETTY_PRINT
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
107
config/packages/fos_rest.yaml
Normal file
107
config/packages/fos_rest.yaml
Normal 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/* }
|
||||
12
config/packages/jms_serializer.yaml
Normal file
12
config/packages/jms_serializer.yaml
Normal 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"
|
||||
@@ -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 }
|
||||
|
||||
27
config/packages/nelmio_api_doc.yaml
Normal file
27
config/packages/nelmio_api_doc.yaml
Normal 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: []
|
||||
6
config/packages/prod/jms_serializer.yaml
Normal file
6
config/packages/prod/jms_serializer.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
@@ -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 }
|
||||
|
||||
3
config/packages/sensio_framework_extra.yaml
Normal file
3
config/packages/sensio_framework_extra.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
sensio_framework_extra:
|
||||
router:
|
||||
annotations: false
|
||||
@@ -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"
|
||||
|
||||
@@ -5,4 +5,9 @@ controllers:
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
_locale: '%locale%'
|
||||
|
||||
app.api:
|
||||
resource: '../src/API/'
|
||||
type: rest
|
||||
prefix: /api
|
||||
|
||||
12
config/routes/fos_user.yaml
Normal file
12
config/routes/fos_user.yaml
Normal 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
|
||||
11
config/routes/nelmio_api_doc.yaml
Normal file
11
config/routes/nelmio_api_doc.yaml
Normal 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 }
|
||||
18
config/serializer/App/Entity.Activity.yml
Normal file
18
config/serializer/App/Entity.Activity.yml
Normal 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
|
||||
35
config/serializer/App/Entity.Customer.yml
Normal file
35
config/serializer/App/Entity.Customer.yml
Normal 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
|
||||
22
config/serializer/App/Entity.Project.yml
Normal file
22
config/serializer/App/Entity.Project.yml
Normal 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
|
||||
11
config/serializer/App/Entity.User.yml
Normal file
11
config/serializer/App/Entity.User.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
App\Entity\User:
|
||||
exclusion_policy: All
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
alias:
|
||||
include: true
|
||||
title:
|
||||
include: true
|
||||
avatar:
|
||||
include: true
|
||||
23
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
23
config/serializer/FOS/UserBundle/Model.User.yml
Normal 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
|
||||
@@ -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']
|
||||
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ### -->
|
||||
|
||||
@@ -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 |
@@ -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",
|
||||
|
||||
89
src/API/ActivityController.php
Normal file
89
src/API/ActivityController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
89
src/API/CustomerController.php
Normal file
89
src/API/CustomerController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
49
src/API/HealthcheckController.php
Normal file
49
src/API/HealthcheckController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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);
|
||||
}
|
||||
}
|
||||
22
src/API/NotFoundException.php
Normal file
22
src/API/NotFoundException.php
Normal 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);
|
||||
}
|
||||
}
|
||||
89
src/API/ProjectController.php
Normal file
89
src/API/ProjectController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
90
src/API/UserController.php
Normal file
90
src/API/UserController.php
Normal 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
66
src/Calendar/Config.php
Normal 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
48
src/Calendar/Google.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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
59
src/Calendar/Service.php
Normal 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
83
src/Calendar/Source.php
Normal 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;
|
||||
}
|
||||
}
|
||||
268
src/Calendar/TimesheetEntity.php
Normal file
268
src/Calendar/TimesheetEntity.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
102
src/Controller/CalendarController.php
Normal file
102
src/Controller/CalendarController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
132
src/DataFixtures/CustomerFixtures.php
Normal file
132
src/DataFixtures/CustomerFixtures.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
;
|
||||
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
];
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
33
src/DependencyInjection/Compiler/TwigContextCompilerPass.php
Normal file
33
src/DependencyInjection/Compiler/TwigContextCompilerPass.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -69,6 +69,14 @@ class Activity
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheets(): array
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
63
src/Event/DashboardEvent.php
Normal file
63
src/Event/DashboardEvent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
139
src/EventSubscriber/DashboardSubscriber.php
Normal file
139
src/EventSubscriber/DashboardSubscriber.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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', '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'csrf_token_id' => 'timesheet_edit',
|
||||
'duration_only' => false,
|
||||
'include_user' => false,
|
||||
'documentation' => 'timesheet',
|
||||
'docu_chapter' => 'timesheet',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
50
src/Form/UserApiTokenType.php
Normal file
50
src/Form/UserApiTokenType.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
94
src/Migrations/Version20180730044139.php
Normal file
94
src/Migrations/Version20180730044139.php
Normal 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)');
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/Migrations/Version20180805183527.php
Normal file
79
src/Migrations/Version20180805183527.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/Model/DashboardSection.php
Normal file
106
src/Model/DashboardSection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
219
src/Model/Widget.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user