recent activities via API (#761)
This commit is contained in:
@@ -25,6 +25,8 @@ import KimaiSelectDataAPI from "./plugins/KimaiSelectDataAPI";
|
||||
import KimaiDateTimePicker from "./plugins/KimaiDateTimePicker";
|
||||
import KimaiAlternativeLinks from "./plugins/KimaiAlternativeLinks";
|
||||
import KimaiAjaxModalForm from "./plugins/KimaiAjaxModalForm";
|
||||
import KimaiRecentActivities from "./plugins/KimaiRecentActivities";
|
||||
import KimaiEvent from "./plugins/KimaiEvent";
|
||||
|
||||
export default class KimaiLoader {
|
||||
|
||||
@@ -60,6 +62,7 @@ export default class KimaiLoader {
|
||||
new KimaiTranslation(translations)
|
||||
);
|
||||
|
||||
kimai.registerPlugin(new KimaiEvent());
|
||||
kimai.registerPlugin(new KimaiAPI());
|
||||
kimai.registerPlugin(new KimaiActiveRecordsDuration('[data-since]'));
|
||||
kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility'));
|
||||
@@ -73,19 +76,16 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
|
||||
kimai.registerPlugin(new KimaiRecentActivities('li.notifications-menu'));
|
||||
|
||||
// notify all listeners that Kimai plugins can now be registered
|
||||
this._sendEvent('kimai.pluginRegister');
|
||||
kimai.getPlugin('event').trigger('kimai.pluginRegister');
|
||||
|
||||
// initialize all plugins
|
||||
kimai.getPlugins().map(plugin => { plugin.init(); });
|
||||
|
||||
// notify all listeners that Kimai is now ready to be used
|
||||
this._sendEvent('kimai.initialized');
|
||||
}
|
||||
|
||||
_sendEvent(name) {
|
||||
document.dispatchEvent(new Event(name));
|
||||
kimai.getPlugin('event').trigger('kimai.initialized');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
|
||||
self._openFormInModal(html);
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
console.log('Failed opening modal', err);
|
||||
window.location = href;
|
||||
}
|
||||
});
|
||||
@@ -102,8 +101,12 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
|
||||
// click handler for modal save button, to send forms via ajax
|
||||
form.on('submit', function(event){
|
||||
let btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
|
||||
let eventName = form.attr('data-form-event');
|
||||
let events = self.getContainer().getPlugin('event');
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
jQuery.ajax({
|
||||
url: form.attr('action'),
|
||||
type: form.attr('method'),
|
||||
@@ -117,14 +120,13 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
|
||||
if (hasFieldError || hasFormError || hasFlashError) {
|
||||
self._openFormInModal(html);
|
||||
} else {
|
||||
events.trigger(eventName);
|
||||
self.getContainer().getPlugin('datatable').reload();
|
||||
remoteModal.modal('hide');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
console.log('Failed submitting modal form', err);
|
||||
|
||||
// FIXME problem in google and 500 error, keeps on submitting...
|
||||
// what else could we do? submitting again at least gives us the opportunity to see errors,
|
||||
// which maybe would be hidden otherwise... this one is totally up for discussion!
|
||||
|
||||
@@ -18,13 +18,6 @@ export default class KimaiDatatable extends KimaiPlugin {
|
||||
return 'datatable';
|
||||
}
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
document.addEventListener('KimaiDatatableRequestReload', function() {
|
||||
self.reload();
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
let form = jQuery('.toolbar form');
|
||||
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
|
||||
|
||||
@@ -83,7 +83,6 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
|
||||
}
|
||||
|
||||
if (!foundColumn) {
|
||||
console.error('Could not find column: ' + columnName);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
28
assets/js/plugins/KimaiEvent.js
Normal file
28
assets/js/plugins/KimaiEvent.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiEvent: helper to trigger events
|
||||
*/
|
||||
|
||||
import KimaiPlugin from "../KimaiPlugin";
|
||||
|
||||
export default class KimaiEvent extends KimaiPlugin {
|
||||
|
||||
getId() {
|
||||
return 'event';
|
||||
}
|
||||
|
||||
trigger(name) {
|
||||
if (name === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new Event(name));
|
||||
}
|
||||
|
||||
}
|
||||
81
assets/js/plugins/KimaiRecentActivities.js
Normal file
81
assets/js/plugins/KimaiRecentActivities.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
|
||||
*/
|
||||
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
|
||||
export default class KimaiRecentActivities extends KimaiPlugin {
|
||||
|
||||
constructor(selector) {
|
||||
super();
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'recent-activities';
|
||||
}
|
||||
|
||||
init() {
|
||||
const menu = document.querySelector(this.selector);
|
||||
// the menu can be hidden if user has no permissions to see it
|
||||
if (menu === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdown = menu.querySelector('ul.dropdown-menu');
|
||||
|
||||
this.attributes = dropdown.dataset;
|
||||
this.itemList = dropdown.querySelector('li > ul.menu');
|
||||
|
||||
const self = this;
|
||||
const handle = function() { self.reload(); };
|
||||
|
||||
// don't block initial browser rendering
|
||||
setTimeout(handle, 500);
|
||||
|
||||
document.addEventListener('kimai.activityUpdate', handle);
|
||||
document.addEventListener('kimai.projectUpdate', handle);
|
||||
document.addEventListener('kimai.customerUpdate', handle);
|
||||
}
|
||||
|
||||
emptyList() {
|
||||
this.itemList.innerHTML = '';
|
||||
}
|
||||
|
||||
setEntries(entries) {
|
||||
if (entries.length === 0) {
|
||||
this.emptyList();
|
||||
return;
|
||||
}
|
||||
|
||||
let htmlToInsert = '';
|
||||
|
||||
for (let timesheet of entries) {
|
||||
let label = this.attributes['template']
|
||||
.replace('%customer%', timesheet.project.customer.name)
|
||||
.replace('%project%', timesheet.project.name)
|
||||
.replace('%activity%', timesheet.activity.name);
|
||||
|
||||
htmlToInsert += `<li><a href="${ this.attributes['url'].replace('000', timesheet.id) }"><i class="${ this.attributes['icon'] }"></i> ${ label }</a></li>`;
|
||||
}
|
||||
|
||||
this.itemList.innerHTML = htmlToInsert;
|
||||
}
|
||||
|
||||
reload() {
|
||||
const self = this;
|
||||
const apiService = this.getContainer().getPlugin('api');
|
||||
|
||||
apiService.get(this.attributes['api'], function(result) {
|
||||
self.setEntries(result);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,8 +10,13 @@
|
||||
padding: 16px 12px 11px 12px;
|
||||
font-size: 10px;
|
||||
}
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
li.messages-menu ul.menu li {
|
||||
a:hover .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
a .pull-left i {
|
||||
color: #444;
|
||||
}
|
||||
}
|
||||
.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
@@ -33,6 +38,12 @@
|
||||
ul {
|
||||
li {
|
||||
a {
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
i {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
color: #444;
|
||||
i {
|
||||
padding-right: 10px;
|
||||
@@ -44,6 +55,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.notifications-menu>.dropdown-menu>li .menu>li>a:hover i {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
// Primary
|
||||
//$light-blue: #3c8dbc;
|
||||
// Danger
|
||||
//$red: #dd4b39;
|
||||
$red: #dd4b39;
|
||||
// Success
|
||||
//$green: #00a65a;
|
||||
$green: #00a65a;
|
||||
// Info
|
||||
//$aqua: #00c0ef;
|
||||
// Warning
|
||||
//$yellow: #f39c12;
|
||||
//$blue: #0073b7;
|
||||
$blue: #0073b7;
|
||||
//$navy: #001F3F;
|
||||
//$teal: #39CCCC;
|
||||
//$olive: #3D9970;
|
||||
|
||||
@@ -2,21 +2,22 @@ nelmio_api_doc:
|
||||
models:
|
||||
use_jms: true
|
||||
names:
|
||||
- { alias: CustomerEditForm, type: App\Form\CustomerEditForm, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
||||
- { alias: ProjectEditForm, type: App\Form\ProjectEditForm, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
||||
- { alias: ActivityEditForm, type: App\Form\ActivityEditForm, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
||||
- { alias: TimesheetEditForm, type: App\Form\TimesheetEditForm, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
||||
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
||||
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
||||
- { alias: CustomerEditForm, type: App\Form\CustomerEditForm, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
||||
- { alias: ProjectEditForm, type: App\Form\ProjectEditForm, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
||||
- { alias: ActivityEditForm, type: App\Form\ActivityEditForm, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
||||
- { alias: TimesheetEditForm, type: App\Form\TimesheetEditForm, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
||||
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
||||
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
||||
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
app.test.activity_recent:
|
||||
path: /{_locale}/activities/recent
|
||||
controller: App\Controller\NavbarController::recentActivitiesAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
app.test.sidebar_settings:
|
||||
path: /{_locale}/sidebar/settings
|
||||
controller: App\Controller\SidebarController::settingsAction
|
||||
|
||||
@@ -16,10 +16,10 @@ App\Entity\Activity:
|
||||
groups: [Default]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups: [Activity]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups: [Activity]
|
||||
project:
|
||||
include: false
|
||||
exclude: true
|
||||
|
||||
@@ -4,10 +4,8 @@ App\Entity\Customer:
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
number:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
@@ -16,7 +14,6 @@ App\Entity\Customer:
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
company:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
@@ -52,10 +49,10 @@ App\Entity\Customer:
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups: [Customer]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups: [Customer]
|
||||
color:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
|
||||
@@ -4,16 +4,13 @@ App\Entity\Project:
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
comment:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
budget:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
@@ -22,14 +19,12 @@ App\Entity\Project:
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups: [Project]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups: [Project]
|
||||
customer:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
groups: [Subresource]
|
||||
color:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
@@ -38,4 +33,4 @@ App\Entity\Project:
|
||||
serialized_name: customer
|
||||
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
groups: [Entity, Collection]
|
||||
|
||||
@@ -4,20 +4,16 @@ App\Entity\Timesheet:
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
begin:
|
||||
exclude: true
|
||||
end:
|
||||
exclude: true
|
||||
duration:
|
||||
include: true
|
||||
groups: [Default]
|
||||
description:
|
||||
include: true
|
||||
groups: [Default]
|
||||
rate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
@@ -28,9 +24,9 @@ App\Entity\Timesheet:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
activity:
|
||||
exclude: true
|
||||
groups: [Subresource]
|
||||
project:
|
||||
exclude: true
|
||||
groups: [Subresource]
|
||||
user:
|
||||
exclude: true
|
||||
virtual_properties:
|
||||
@@ -38,24 +34,21 @@ App\Entity\Timesheet:
|
||||
serialized_name: begin
|
||||
exp: "object.getBegin() === null ? null : object.getBegin()"
|
||||
type: DateTime
|
||||
groups: [Default]
|
||||
getEnd:
|
||||
serialized_name: end
|
||||
exp: "object.getEnd() === null ? null : object.getEnd()"
|
||||
type: DateTime
|
||||
groups: [Default]
|
||||
getActivity:
|
||||
serialized_name: activity
|
||||
exp: "object.getActivity() === null ? null : object.getActivity().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
groups: [Entity, Collection]
|
||||
getProject:
|
||||
serialized_name: project
|
||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
groups: [Entity, Collection]
|
||||
getUser:
|
||||
serialized_name: user
|
||||
exp: "object.getUser().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.js": "./app.js?513aa9647de2030610d0",
|
||||
"build/app.css": "./app.css?b3b98e5f8744bf95f3f8d8352977d4f3",
|
||||
"build/app.js": "./app.js?4ea85f1b6fd659da9391",
|
||||
"build/app.css": "./app.css?6e97182b57b875864bd53ce2cfb93391",
|
||||
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",
|
||||
"build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb",
|
||||
"build/images/glyphicons-halflings-regular.svg": "./images/glyphicons-halflings-regular.svg?89889688",
|
||||
|
||||
@@ -96,7 +96,9 @@ class TimesheetController extends BaseApiController
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
@@ -392,4 +394,53 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection of recent user activities
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of recent user activities (always the latest entry of a unique working set groued by customer, project and activity)",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/TimesheetSubCollection")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Rest\QueryParam(name="user", requirements="\d+|all", strict=true, nullable=true, description="User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")
|
||||
* @Rest\QueryParam(name="begin", requirements=@Constraints\DateTime, strict=true, nullable=true, description="Only records after this date will be included. Default: today - 1 year (format: ISO 8601)")
|
||||
* @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries (default: 10)")
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function recentAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$begin = $this->dateTime->createDateTime('-1 year');
|
||||
$limit = 10;
|
||||
|
||||
if ($this->isGranted('view_other_timesheet') && null !== ($reqUser = $paramFetcher->get('user'))) {
|
||||
if ('all' === $reqUser) {
|
||||
$reqUser = null;
|
||||
}
|
||||
$user = $reqUser;
|
||||
}
|
||||
|
||||
if (null !== ($reqLimit = $paramFetcher->get('size'))) {
|
||||
$limit = $reqLimit;
|
||||
}
|
||||
|
||||
if (null !== ($reqBegin = $paramFetcher->get('begin'))) {
|
||||
$begin = new \DateTime($reqBegin);
|
||||
}
|
||||
|
||||
$data = $this->repository->getRecentActivities($user, $begin, $limit);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Subresource', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Controller used to render recent activities and quick-start new recordings in the navigation-bar.
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class NavbarController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param ActivityRepository $repository
|
||||
* @param UserDateTimeFactory $dateTimeFactory
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function recentActivitiesAction(ActivityRepository $repository, UserDateTimeFactory $dateTimeFactory)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$entries = $repository->getRecentActivities($user, $dateTimeFactory->createDateTime('-1 year'));
|
||||
|
||||
return $this->render(
|
||||
'navbar/recent-activities.html.twig',
|
||||
['entries' => $entries]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,9 @@ class SystemConfigurationController extends AbstractController
|
||||
*/
|
||||
protected function handleConfigUpdate(Request $request, string $section)
|
||||
{
|
||||
$configModel = null;
|
||||
$configSettings = $this->getInitializedConfigurations();
|
||||
|
||||
foreach ($configSettings as $configModel) {
|
||||
if ($configModel->getSection() === $section) {
|
||||
break;
|
||||
|
||||
@@ -144,6 +144,9 @@ class ActivityEditForm extends AbstractType
|
||||
'csrf_token_id' => 'admin_activity_edit',
|
||||
'create_more' => false,
|
||||
'customer' => false,
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.activityUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,9 @@ class CustomerEditForm extends AbstractType
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_customer_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,9 @@ class ProjectEditForm extends AbstractType
|
||||
'csrf_token_id' => 'admin_project_edit',
|
||||
'currency' => Customer::DEFAULT_CURRENCY,
|
||||
'create_more' => false,
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace App\Repository;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\ActivityStatistic;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
@@ -31,63 +30,6 @@ class ActivityRepository extends AbstractRepository
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user
|
||||
* @param \DateTime|null $startFrom
|
||||
* @return Timesheet[]
|
||||
* @throws Query\QueryException
|
||||
*/
|
||||
public function getRecentActivities(User $user = null, \DateTime $startFrom = null)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select($qb->expr()->max('t.id') . ' AS maxid')
|
||||
->from(Timesheet::class, 't')
|
||||
->indexBy('t', 't.id')
|
||||
->join('t.activity', 'a')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->eq('a.visible', ':visible'))
|
||||
->andWhere($qb->expr()->eq('p.visible', ':visible'))
|
||||
->andWhere($qb->expr()->eq('c.visible', ':visible'))
|
||||
->groupBy('a.id', 'p.id')
|
||||
->orderBy('maxid', 'DESC')
|
||||
->setMaxResults(10)
|
||||
->setParameter('visible', true, \PDO::PARAM_BOOL)
|
||||
;
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
if (null !== $startFrom) {
|
||||
$qb->andWhere($qb->expr()->gt('t.begin', ':begin'))
|
||||
->setParameter('begin', $startFrom);
|
||||
}
|
||||
|
||||
$results = $qb->getQuery()->getScalarResult();
|
||||
|
||||
if (empty($results)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = array_column($results, 'maxid');
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('t', 'a', 'p', 'c')
|
||||
->from(Timesheet::class, 't')
|
||||
->join('t.activity', 'a')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->orderBy('t.end', 'DESC')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|bool $visible
|
||||
* @return int
|
||||
|
||||
@@ -384,4 +384,62 @@ class TimesheetRepository extends AbstractRepository
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user
|
||||
* @param DateTime|null $startFrom
|
||||
* @param int $limit
|
||||
* @return array|mixed
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
public function getRecentActivities(User $user = null, \DateTime $startFrom = null, $limit = 10)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select($qb->expr()->max('t.id') . ' AS maxid')
|
||||
->from(Timesheet::class, 't')
|
||||
->indexBy('t', 't.id')
|
||||
->join('t.activity', 'a')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->eq('a.visible', ':visible'))
|
||||
->andWhere($qb->expr()->eq('p.visible', ':visible'))
|
||||
->andWhere($qb->expr()->eq('c.visible', ':visible'))
|
||||
->groupBy('a.id', 'p.id')
|
||||
->orderBy('maxid', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setParameter('visible', true, \PDO::PARAM_BOOL)
|
||||
;
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
if (null !== $startFrom) {
|
||||
$qb->andWhere($qb->expr()->gt('t.begin', ':begin'))
|
||||
->setParameter('begin', $startFrom);
|
||||
}
|
||||
|
||||
$results = $qb->getQuery()->getScalarResult();
|
||||
|
||||
if (empty($results)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = array_column($results, 'maxid');
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('t', 'a', 'p', 'c')
|
||||
->from(Timesheet::class, 't')
|
||||
->join('t.activity', 'a')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->andWhere($qb->expr()->in('t.id', $ids))
|
||||
->orderBy('t.end', 'DESC')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class Extensions extends AbstractExtension
|
||||
'project' => 'fas fa-project-diagram',
|
||||
'repeat' => 'fas fa-redo-alt',
|
||||
'start' => 'fas fa-play-circle',
|
||||
'start-small' => 'fas fa-play-circle',
|
||||
'start-small' => 'far fa-play-circle',
|
||||
'stop' => 'fas fa-stop',
|
||||
'stop-small' => 'far fa-stop-circle',
|
||||
'timesheet' => 'fas fa-clock',
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
{% block navbar_user %}
|
||||
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
|
||||
{{ render(controller('App\\Controller\\NavbarController::recentActivitiesAction')) }}
|
||||
{% include 'navbar/recent-activities.html.twig' %}
|
||||
{% endif %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
<li class="dropdown user-menu">
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
{% if entries is defined and entries is not empty and is_granted('start_own_timesheet') %}
|
||||
{% if is_granted('start_own_timesheet') %}
|
||||
<li class="dropdown notifications-menu">
|
||||
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown">
|
||||
<i class="{{ 'activity'|icon }} fa-2x"></i>
|
||||
<span class="label label-success">{{ entries|length }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header">{{ 'recent.activities'|trans({'%count%':(entries|length)}) }}</li>
|
||||
<ul class="dropdown-menu"
|
||||
data-api="{{ path('recent_timesheet', {'size' : 10}) }}"
|
||||
data-url="{{ path('timesheet_start', {'id' : '000'}) }}"
|
||||
data-icon="{{ 'start-small'|icon }}"
|
||||
data-template="{{ 'recent.activities.format'|trans }}">
|
||||
<li class="header">{{ 'recent.activities'|trans }}</li>
|
||||
<li>
|
||||
<ul class="menu">
|
||||
{% for timesheet in entries %}
|
||||
<li>
|
||||
<a href="{{ path('timesheet_start', {'id' : timesheet.id}) }}">
|
||||
<i class="{{ 'start-small'|icon }} text-green"></i> {{ 'recent.activities.format'|trans({'%activity%': timesheet.activity.name, '%project%': timesheet.project.name, '%customer%': timesheet.project.customer.name}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="footer"><a href="{{ url('timesheet') }}">{{ 'timesheet.all'|trans }}</a></li>
|
||||
|
||||
@@ -22,6 +22,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*/
|
||||
class TimesheetControllerTest extends APIControllerBaseTest
|
||||
{
|
||||
public const DATE_FORMAT = 'Y-m-d H:i:s';
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->importFixtureForUser(User::ROLE_USER);
|
||||
@@ -460,6 +462,39 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$this->assertEquals('You are not allowed to delete this timesheet', $json['message']);
|
||||
}
|
||||
|
||||
public function testGetRecentCollectionWithSubresources()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$start = new \DateTime('-10 days');
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture
|
||||
->setFixedRate(true)
|
||||
->setHourlyRate(true)
|
||||
->setAmount(10)
|
||||
->setUser($this->getUserByRole($em, User::ROLE_ADMIN))
|
||||
->setStartDate($start)
|
||||
;
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$query = [
|
||||
'user' => 'all',
|
||||
'size' => 2,
|
||||
'begin' => $start->format(self::DATE_FORMAT),
|
||||
];
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/timesheets/recent', 'GET', $query);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(1, count($result));
|
||||
$this->assertDefaultStructure($result[0], false);
|
||||
$this->assertHasSubresources($result[0]);
|
||||
}
|
||||
|
||||
protected function assertDefaultStructure(array $result, $full = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
@@ -478,4 +513,23 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
|
||||
$this->assertEquals($expectedKeys, $actual, 'Timesheet structure does not match');
|
||||
}
|
||||
|
||||
protected function assertHasSubresources(array $result)
|
||||
{
|
||||
$this->assertArrayHasKey('activity', $result);
|
||||
$this->assertArrayHasKey('id', $result['activity']);
|
||||
$this->assertArrayHasKey('name', $result['activity']);
|
||||
$this->assertArrayHasKey('visible', $result['activity']);
|
||||
$this->assertArrayHasKey('project', $result['activity']);
|
||||
|
||||
$this->assertArrayHasKey('project', $result);
|
||||
$this->assertArrayHasKey('id', $result['project']);
|
||||
$this->assertArrayHasKey('name', $result['project']);
|
||||
$this->assertArrayHasKey('visible', $result['project']);
|
||||
$this->assertArrayHasKey('customer', $result['project']);
|
||||
$this->assertArrayHasKey('id', $result['project']['customer']);
|
||||
$this->assertArrayHasKey('name', $result['project']['customer']);
|
||||
$this->assertArrayHasKey('visible', $result['project']['customer']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\NavbarController
|
||||
* @group integration
|
||||
*/
|
||||
class NavbarControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/activities/recent');
|
||||
}
|
||||
|
||||
public function testRecentActivitiesAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($user);
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/activities/recent');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
|
||||
$this->assertContains('<li class="dropdown notifications-menu">', $content);
|
||||
$this->assertContains('<span class="label label-success">1</span>', $content);
|
||||
$this->assertContains('<a href="/en/timesheet/start/1">', $content);
|
||||
}
|
||||
}
|
||||
@@ -808,7 +808,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
إعادة تشغيل آخر نشاط مسجل الخاص بك، إعادة تشغيل واحد من %count% الأنشطة الأخيرة
|
||||
إعادة تشغيل آخر نشاط مسجل الخاص بك، إعادة تشغيل واحد من الأنشطة الأخيرة
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -927,7 +927,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Ihre letzte Tätigkeit wiederaufnehmen|Eine ihrer %count% letzten Tätigkeiten neustarten
|
||||
Eine ihrer letzten Tätigkeiten neustarten
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -927,7 +927,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Restart your last recorded activity|Restart one of your %count% last activities
|
||||
Restart one of your last activities
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -752,7 +752,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Reiniciar su última actividad registrada|Reiniciar una de sus %count% ultimas actividades
|
||||
Reiniciar una de sus ultimas actividades
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -785,7 +785,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Redémarrez l'enregistrement de la dernière activité|Redémarrez l'enregistrement d'une de vos %count% dernières activités
|
||||
Redémarrez l'enregistrement d'une de vos dernières activités
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Legutóbb rögzített tevékenység folytatása|Tevékenység folytatása a legutóbbi %count% közül
|
||||
Tevékenység folytatása a legutóbbi közül
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -730,7 +730,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Ricomincia registrazione|Ricomincia una delle %count% tue ultime attività
|
||||
Ricomincia una delle tue ultime attività
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -829,7 +829,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Reinicie a sua última atividade registrada|Reinicie uma de suas %count% últimas atividades
|
||||
Reinicie uma de suas últimas atividades
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -776,7 +776,7 @@
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>
|
||||
Возобновить последнюю деятельность|Возобновить одну их %count% последних деятельностей.|Возобновить одну их %count% последних деятельностей.
|
||||
Возобновить одну их последних деятельностей.
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
|
||||
@@ -941,8 +941,7 @@ För närvarande har %user% användare %records% tidsrekord som räknas upp till
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target state="translated">
|
||||
Starta din senaste registrerade aktiviteten|starta en av dina %count% sista aktiviteter</target>
|
||||
<target state="translated">starta en av dina sista aktiviteter</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
<source>recent.activities.format</source>
|
||||
|
||||
Reference in New Issue
Block a user