* added delete activity form #71 * fixed stop button, added permission checks around action buttons #70 * do not allow to start hidden activities #33 * do not allow to start record for hidden project or customer #27 #32
This commit is contained in:
@@ -39,6 +39,11 @@ class DashboardController extends Controller
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
$userStats = $this->getDoctrine()->getRepository(User::class)->getGlobalStatistics();
|
||||
|
||||
// FIXME move the other widgets to the TimesheetBundle, the inheritence is wrong as AppBundle
|
||||
// shouldn't know about Timesheets
|
||||
|
||||
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
$timesheetUserStats = $timesheetRepo->getUserStatistics($user);
|
||||
$timesheetGlobalStats = $timesheetRepo->getGlobalStatistics();
|
||||
@@ -46,7 +51,6 @@ class DashboardController extends Controller
|
||||
$activityStats = $this->getDoctrine()->getRepository(Activity::class)->getGlobalStatistics();
|
||||
$projectStats = $this->getDoctrine()->getRepository(Project::class)->getGlobalStatistics();
|
||||
$customerStats = $this->getDoctrine()->getRepository(Customer::class)->getGlobalStatistics();
|
||||
$userStats = $this->getDoctrine()->getRepository(User::class)->getGlobalStatistics();
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'dashboard_widgets' => $this->getWidgets(),
|
||||
@@ -129,7 +133,7 @@ class DashboardController extends Controller
|
||||
"{{ widgets.info_box_more('stats.userTotal', user.totalAmount, ' ', path('admin_user'), 'user') }}",
|
||||
"{{ widgets.info_box_more('stats.customerTotal', customer.totalAmount, '', path('admin_customer'), 'users', 'blue') }}",
|
||||
"{{ widgets.info_box_more('stats.projectsTotal', project.totalAmount, '', path('admin_project'), 'book', 'yellow') }}",
|
||||
"{{ widgets.info_box_more('stats.activitiesTotal', activity.totalAmount, '', path('admin_activity'), 'tasks', 'purple') }}",
|
||||
"{{ widgets.info_box_more('stats.activitiesTotal', activity.count, '', path('admin_activity'), 'tasks', 'purple') }}",
|
||||
],
|
||||
];
|
||||
// @codingStandardsIgnoreEnd
|
||||
|
||||
@@ -21,6 +21,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use TimesheetBundle\Entity\Customer;
|
||||
use TimesheetBundle\Entity\Project;
|
||||
use TimesheetBundle\Form\ActivityDeleteForm;
|
||||
use TimesheetBundle\Form\ActivityEditForm;
|
||||
use TimesheetBundle\Form\Toolbar\ActivityToolbarForm;
|
||||
use TimesheetBundle\Repository\Query\ActivityQuery;
|
||||
@@ -35,6 +36,15 @@ use TimesheetBundle\Repository\Query\ActivityQuery;
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
|
||||
/**
|
||||
* @return \TimesheetBundle\Repository\ActivityRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->getDoctrine()->getRepository(Activity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return ActivityQuery
|
||||
@@ -88,7 +98,7 @@ class ActivityController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getDoctrine()->getRepository(Activity::class)->findByQuery($query);
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('TimesheetBundle:admin:activity.html.twig', [
|
||||
'entries' => $entries,
|
||||
@@ -116,6 +126,48 @@ class ActivityController extends AbstractController
|
||||
return $this->renderActivityForm($activity, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route("/{id}/delete", name="admin_activity_delete")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function deleteAction(Activity $activity, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
|
||||
$deleteForm = $this->createForm(ActivityDeleteForm::class, $activity, [
|
||||
'action' => $this->generateUrl('admin_activity_delete', ['id' => $activity->getId()]),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if ($stats->getRecordAmount() == 0 || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'TimesheetBundle:admin:activity_delete.html.twig',
|
||||
[
|
||||
'activity' => $activity,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
|
||||
@@ -38,6 +38,7 @@ class Activity
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="TimesheetBundle\Entity\Project", inversedBy="activities")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
@@ -65,6 +66,31 @@ class Activity
|
||||
*/
|
||||
private $visible = true;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="TimesheetBundle\Entity\Timesheet", mappedBy="activity")
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheets()
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @return Activity
|
||||
*/
|
||||
public function setTimesheets($timesheets)
|
||||
{
|
||||
$this->timesheets = $timesheets;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
@@ -80,7 +106,6 @@ class Activity
|
||||
public function setProject($project)
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -93,7 +118,6 @@ class Activity
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -139,7 +163,6 @@ class Activity
|
||||
public function setVisible($visible)
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class Project
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="TimesheetBundle\Entity\Customer", inversedBy="projects")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
@@ -76,11 +77,7 @@ class Project
|
||||
/**
|
||||
* @var Activity[]
|
||||
*
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="TimesheetBundle\Entity\Activity",
|
||||
* mappedBy="project",
|
||||
* cascade={"persist", "merge", "remove"}
|
||||
* )
|
||||
* @ORM\OneToMany(targetEntity="TimesheetBundle\Entity\Activity", mappedBy="project")
|
||||
*/
|
||||
private $activities;
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
/**
|
||||
* Timesheet entity.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="TimesheetBundle\Repository\TimesheetRepository")
|
||||
* @ORM\Table(
|
||||
* name="timesheet",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"user"}),
|
||||
* @ORM\Index(name="activity", columns={"activity"})
|
||||
* @ORM\Index(columns={"activity_id"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="TimesheetBundle\Repository\TimesheetRepository")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
@@ -76,8 +76,8 @@ class Timesheet
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="TimesheetBundle\Entity\Activity")
|
||||
* @ORM\JoinColumn(name="activity", referencedColumnName="id")
|
||||
* @ORM\ManyToOne(targetEntity="TimesheetBundle\Entity\Activity", inversedBy="timesheets")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
38
src/TimesheetBundle/Form/ActivityDeleteForm.php
Normal file
38
src/TimesheetBundle/Form/ActivityDeleteForm.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
|
||||
/**
|
||||
* The form used to delete Activities.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ActivityDeleteForm extends AbstractType
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Activity::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_activity_edit',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -21,21 +21,73 @@ class ActivityStatistic
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $totalAmount = 0;
|
||||
protected $count = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $recordAmount = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $recordDuration = 0;
|
||||
|
||||
/**
|
||||
* Returns the total amount of included timesheet records.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalAmount()
|
||||
public function getRecordAmount()
|
||||
{
|
||||
return $this->totalAmount;
|
||||
return $this->recordAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $totalAmount
|
||||
* @param int $recordAmount
|
||||
* @return ActivityStatistic
|
||||
*/
|
||||
public function setTotalAmount($totalAmount)
|
||||
public function setRecordAmount($recordAmount)
|
||||
{
|
||||
$this->totalAmount = $totalAmount;
|
||||
$this->recordAmount = (int) $recordAmount;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total duration of all included timesheet records.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRecordDuration()
|
||||
{
|
||||
return $this->recordDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $recordDuration
|
||||
* @return ActivityStatistic
|
||||
*/
|
||||
public function setRecordDuration($recordDuration)
|
||||
{
|
||||
$this->recordDuration = (int) $recordDuration;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of activities that are included in the statistic result.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCount()
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $count
|
||||
* @return ActivityStatistic
|
||||
*/
|
||||
public function setCount($count)
|
||||
{
|
||||
$this->count = (int) $count;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace TimesheetBundle\Repository;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use AppBundle\Repository\AbstractRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use TimesheetBundle\Model\ActivityStatistic;
|
||||
@@ -80,7 +81,7 @@ class ActivityRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Return statistic data for all user.
|
||||
* Return global statistic data for all user.
|
||||
*
|
||||
* @return ActivityStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
@@ -92,7 +93,38 @@ class ActivityRepository extends AbstractRepository
|
||||
->getSingleScalarResult();
|
||||
|
||||
$stats = new ActivityStatistic();
|
||||
$stats->setTotalAmount($countAll);
|
||||
$stats->setCount($countAll);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves statistics for one activity.
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @return ActivityStatistic
|
||||
*/
|
||||
public function getActivityStatistics(Activity $activity)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('COUNT(t.id) as totalRecords', 'SUM(t.duration) as totalDuration')
|
||||
->from('TimesheetBundle:Timesheet', 't')
|
||||
->where('t.activity = :activity')
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->execute(['activity' => $activity], Query::HYDRATE_ARRAY);
|
||||
|
||||
$stats = new ActivityStatistic();
|
||||
|
||||
if (isset($result[0])) {
|
||||
$dbStats = $result[0];
|
||||
|
||||
$stats->setCount(1);
|
||||
$stats->setRecordAmount($dbStats['totalRecords']);
|
||||
$stats->setRecordDuration($dbStats['totalDuration']);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
{% endif %}
|
||||
|
||||
{{ tables.data_table_header({
|
||||
'label.id': 'hidden-xs',
|
||||
'label.name': '',
|
||||
'label.customer': '',
|
||||
'label.project': '',
|
||||
@@ -23,7 +22,6 @@
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td class="hidden-xs">{{ entry.id }}</td>
|
||||
<td>{{ entry.name }}</td>
|
||||
<td>
|
||||
<a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
||||
@@ -34,10 +32,14 @@
|
||||
<td class="hidden-xs">{{ entry.comment }}</td>
|
||||
<td>{{ widgets.label_visible(entry.visible) }}</td>
|
||||
<td>
|
||||
{{ widgets.button_group({
|
||||
'edit': path('admin_activity_edit', {'id': entry.id}),
|
||||
'trash': '#'
|
||||
}) }}
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('admin_activity_edit', {'id': entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_activity_delete', {'id': entry.id})}) %}
|
||||
{% endif %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
|
||||
{% block page_title %}{{ 'admin_activity.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_activity.subtitle'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set params = {
|
||||
'%activity%': '<strong>' ~ activity.name ~ '</strong>',
|
||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
||||
'%count%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
||||
} %}
|
||||
|
||||
{{ include('default/_form_delete.html.twig', {
|
||||
'message': "admin_activity.delete_confirm"|trans(params)|raw,
|
||||
'form': form,
|
||||
'back': path('admin_activity')
|
||||
}) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -42,8 +42,11 @@
|
||||
<td class="hidden-xs"><a href="{{ path('profile'|route_alias, {'username' : entry.user.username}) }}">{{ widgets.label_user(entry.user) }}</a></td>
|
||||
<td class="hidden-xs hidden-sm">{{ entry.description }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {'edit': path('admin_timesheet_edit', {'id' : entry.id, 'page': page})} %}
|
||||
{% if entry.end %}
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('admin_timesheet_edit', {'id' : entry.id, 'page': page})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if not entry.end and is_granted('stop', entry) %}
|
||||
{% set actionButtons = {'stop': path('admin_timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
|
||||
@@ -38,11 +38,18 @@
|
||||
<td class="hidden-xs hidden-sm">{{ widgets.label_activity(entry.activity) }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ entry.description }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})} %}
|
||||
{% set actionButtons = {} %}
|
||||
{% if is_granted('edit', entry) %}
|
||||
{% set actionButtons = {'edit': path('timesheet_edit', {'id' : entry.id, 'page': page})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% if entry.end %}
|
||||
{% set actionButtons = {'repeat': path('timesheet_start', {'id' : entry.activity.id})}|merge(actionButtons) %}
|
||||
{% if is_granted('start', entry.activity) %}
|
||||
{% set actionButtons = {'repeat': path('timesheet_start', {'id' : entry.activity.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set actionButtons = {'stop': path('timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
|
||||
{% if is_granted('stop', entry) %}
|
||||
{% set actionButtons = {'stop': path('timesheet_stop', {'id' : entry.id})}|merge(actionButtons) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if is_granted('delete', entry) %}
|
||||
{% set actionButtons = actionButtons|merge({'trash': path('timesheet_delete', {'id' : entry.id, 'page': page})}) %}
|
||||
|
||||
@@ -103,9 +103,18 @@ class TimesheetVoter extends AbstractVoter
|
||||
protected function canStart(Activity $activity, User $user, TokenInterface $token)
|
||||
{
|
||||
// we could check the amount of active entries
|
||||
// TODO limit to activities that are not hidden
|
||||
|
||||
// if a teamlead starts an entry for another user, check that this user is part of his team
|
||||
|
||||
if (!$activity->getVisible()) {
|
||||
return false;
|
||||
}
|
||||
if (!$activity->getProject()->getVisible()) {
|
||||
return false;
|
||||
}
|
||||
if (!$activity->getProject()->getCustomer()->getVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user