diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index d4152ee6..c713587e 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -62,7 +62,7 @@ kimai: INVOICE: [view_invoice,create_invoice] INVOICE_TEMPLATE: [view_invoice_template,create_invoice_template,edit_invoice_template,delete_invoice_template] TIMESHEET: [view_own_timesheet,start_own_timesheet,stop_own_timesheet,create_own_timesheet,edit_own_timesheet,export_own_timesheet,delete_own_timesheet] - TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,delete_other_timesheet] + TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,export_other_timesheet,delete_other_timesheet] PROFILE: [view_own_profile,edit_own_profile,password_own_profile,preferences_own_profile,api-token_own_profile] PROFILE_OTHER: [view_other_profile,edit_other_profile,delete_other_profile,password_other_profile,roles_other_profile,preferences_other_profile,api-token_other_profile] USER: [view_user,create_user,delete_user] diff --git a/src/Controller/Admin/TimesheetController.php b/src/Controller/Admin/TimesheetController.php index b35ab296..3e48ccca 100644 --- a/src/Controller/Admin/TimesheetController.php +++ b/src/Controller/Admin/TimesheetController.php @@ -78,6 +78,39 @@ class TimesheetController extends AbstractController ]); } + /** + * @Route(path="/export", name="admin_timesheet_export", methods={"GET"}) + * + * @param Request $request + * @return \Symfony\Component\HttpFoundation\Response + */ + public function exportAction(Request $request) + { + $query = new TimesheetQuery(); + $query->setOrder(TimesheetQuery::ORDER_ASC); + + $form = $this->getToolbarForm($query); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var TimesheetQuery $query */ + $query = $form->getData(); + if (null !== $query->getBegin()) { + $query->getBegin()->setTime(0, 0, 0); + } + if (null !== $query->getEnd()) { + $query->getEnd()->setTime(23, 59, 59); + } + } + + /* @var $entries Pagerfanta */ + $entries = $this->getRepository()->findByQuery($query); + + return $this->render('admin/timesheet_export.html.twig', [ + 'entries' => $entries, + 'query' => $query, + ]); + } + /** * @Route(path="/{id}/stop", name="admin_timesheet_stop", methods={"GET"}) * @Security("is_granted('stop', entry)") diff --git a/src/Controller/TimesheetController.php b/src/Controller/TimesheetController.php index f7674a77..2a13a62b 100644 --- a/src/Controller/TimesheetController.php +++ b/src/Controller/TimesheetController.php @@ -88,6 +88,7 @@ class TimesheetController extends AbstractController public function exportAction(Request $request) { $query = new TimesheetQuery(); + $query->setOrder(TimesheetQuery::ORDER_ASC); $form = $this->getToolbarForm($query); $form->handleRequest($request); diff --git a/src/Voter/TimesheetVoter.php b/src/Voter/TimesheetVoter.php index efa21201..c097fea2 100644 --- a/src/Voter/TimesheetVoter.php +++ b/src/Voter/TimesheetVoter.php @@ -23,6 +23,7 @@ class TimesheetVoter extends AbstractVoter public const STOP = 'stop'; public const EDIT = 'edit'; public const DELETE = 'delete'; + public const EXPORT = 'export'; public const VIEW_RATE = 'view_rate'; public const EDIT_RATE = 'edit_rate'; @@ -34,6 +35,7 @@ class TimesheetVoter extends AbstractVoter self::STOP, self::EDIT, self::DELETE, + self::EXPORT, self::VIEW_RATE, self::EDIT_RATE, ]; @@ -89,6 +91,7 @@ class TimesheetVoter extends AbstractVoter case self::STOP: case self::EDIT: case self::DELETE: + case self::EXPORT: $permission .= $attribute; break; diff --git a/templates/admin/timesheet.html.twig b/templates/admin/timesheet.html.twig index de8b42d4..0c42936f 100644 --- a/templates/admin/timesheet.html.twig +++ b/templates/admin/timesheet.html.twig @@ -6,7 +6,11 @@ {% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %} {% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %} {% block page_actions %} - {% set actions = {'filter': '#collapseTimesheetAdmin', 'visibility': '#modal_timesheet_admin'} %} + {% set actions = {'filter': '#collapseTimesheetAdmin'} %} + {% if is_granted('export_own_timesheet') %} + {% set actions = actions|merge({'download': 'onclick:return exportTimesheet()'}) %} + {% endif %} + {% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %} {% if is_granted('create_other_timesheet') %} {% set actions = actions|merge({'create': path('admin_timesheet_create')}) %} {% endif %} @@ -98,3 +102,17 @@ {{ tables.data_table_footer(entries, 'admin_timesheet_paginated') }} {% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/templates/admin/timesheet_export.html.twig b/templates/admin/timesheet_export.html.twig new file mode 100644 index 00000000..02b90e96 --- /dev/null +++ b/templates/admin/timesheet_export.html.twig @@ -0,0 +1,78 @@ +{% import "macros/widgets.html.twig" as widgets %} +{% extends 'invoice/layout.html.twig' %} + +{% block invoice %} + +
+
+ +
+
+ +
+
+ + + + + {% if query.user is empty %} + + {% endif %} + + + + + + {% set timeWorked = 0 %} + {% for entry in entries %} + {% set timeWorked = timeWorked + entry.duration %} + + + {% if query.user is empty %} + + {% endif %} + + + + {% endfor %} + + + + + + + + +
{{ 'label.date'|trans }}{{ 'label.username'|trans }}{{ 'label.description'|trans }}{{ 'label.hours'|trans }}
{{ entry.begin|date_short }}{{ widgets.username(entry.user) }} + {% if entry.description is not empty %} +
+ {{ entry.description|desc2html }} +
+ {% endif %} + + {{ 'label.activity'|trans }}: {{ entry.activity.name }} | + {{ 'label.project'|trans }}: {{ entry.project.name }} | + {{ 'label.customer'|trans }}: {{ entry.project.customer.name }} + +
{{ entry.duration|duration }}
{{ 'invoice.total_working_time'|trans }}{{ timeWorked|duration }}
+
+
+ +{% endblock %} + +{% block print_button %}{% endblock %} \ No newline at end of file diff --git a/tests/Controller/Admin/ActivityControllerTest.php b/tests/Controller/Admin/ActivityControllerTest.php index 8065438d..7c16cfa3 100644 --- a/tests/Controller/Admin/ActivityControllerTest.php +++ b/tests/Controller/Admin/ActivityControllerTest.php @@ -44,12 +44,20 @@ class ActivityControllerTest extends ControllerBaseTest $this->assertNull($form->get('activity_edit_form[create_more]')->getValue()); $client->submit($form, [ 'activity_edit_form' => [ - 'name' => 'Test 2', + 'name' => 'An AcTiVitY Name', + 'project' => '1', ] ]); $this->assertIsRedirect($client, $this->createUrl('/admin/activity/')); $client->followRedirect(); $this->assertHasDataTable($client); + + $this->request($client, '/admin/activity/2/edit'); + $editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form(); + $this->assertEquals('An AcTiVitY Name', $editForm->get('activity_edit_form[name]')->getValue()); + // make sure customer and project are pre-selected for none global activities + $this->assertEquals('1', $editForm->get('activity_edit_form[project]')->getValue()); + $this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue()); } public function testCreateActionWithCreateMore() @@ -101,6 +109,9 @@ class ActivityControllerTest extends ControllerBaseTest $this->request($client, '/admin/activity/1/edit'); $editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form(); $this->assertEquals('Test 2', $editForm->get('activity_edit_form[name]')->getValue()); + // make sure no customer or project is pre-selected for global activities + $this->assertEquals('', $editForm->get('activity_edit_form[customer]')->getValue()); + $this->assertEquals('', $editForm->get('activity_edit_form[project]')->getValue()); } public function testDeleteAction() diff --git a/tests/Controller/Admin/TimesheetControllerTest.php b/tests/Controller/Admin/TimesheetControllerTest.php index eebdb40c..17e4626a 100644 --- a/tests/Controller/Admin/TimesheetControllerTest.php +++ b/tests/Controller/Admin/TimesheetControllerTest.php @@ -33,7 +33,7 @@ class TimesheetControllerTest extends ControllerBaseTest $this->assertHasDataTable($client); $result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn'); - $this->assertEquals(3, count($result)); + $this->assertEquals(4, count($result)); foreach ($result as $item) { $this->assertEquals('btn btn-default', $item->getAttribute('class')); @@ -71,6 +71,44 @@ class TimesheetControllerTest extends ControllerBaseTest // TODO more assertions } + public function testExportAction() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $fixture = new TimesheetFixtures(); + $fixture->setAmount(7); + $fixture->setUser($this->getUserByRole($em, User::ROLE_USER)); + $fixture->setStartDate(new \DateTime('-10 days')); + $this->importFixture($em, $fixture); + $fixture = new TimesheetFixtures(); + $fixture->setAmount(3); + $fixture->setUser($this->getUserByRole($em, User::ROLE_TEAMLEAD)); + $fixture->setStartDate(new \DateTime('-10 days')); + $this->importFixture($em, $fixture); + + $this->request($client, '/team/timesheet/'); + $this->assertTrue($client->getResponse()->isSuccessful()); + + $form = $client->getCrawler()->filter('form.navbar-form')->form(); + $form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export')); + $client->submit($form, [ + 'state' => 1, + 'pageSize' => 25, + 'begin' => (new \DateTime('-10 days'))->format('Y-m-d'), + 'end' => (new \DateTime())->format('Y-m-d'), + 'customer' => null, + ]); + + $this->assertTrue($client->getResponse()->isSuccessful()); + + $node = $client->getCrawler()->filter('body'); + $this->assertEquals('invoice_print', $node->getNode(0)->getAttribute('class')); + + $result = $node->filter('section.invoice table.table tbody tr'); + $this->assertEquals(10, count($result)); + } + public function testCreateAction() { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); diff --git a/tests/Voter/AbstractVoterTest.php b/tests/Voter/AbstractVoterTest.php index 4b052387..207cca30 100644 --- a/tests/Voter/AbstractVoterTest.php +++ b/tests/Voter/AbstractVoterTest.php @@ -70,7 +70,7 @@ abstract class AbstractVoterTest extends TestCase $invoice = ['view_invoice', 'create_invoice']; $invoiceTemplate = ['view_invoice_template', 'create_invoice_template', 'edit_invoice_template', 'delete_invoice_template']; $timesheet = ['view_own_timesheet', 'start_own_timesheet', 'stop_own_timesheet', 'create_own_timesheet', 'edit_own_timesheet', 'export_own_timesheet', 'delete_own_timesheet']; - $timesheetOthers = ['view_other_timesheet', 'start_other_timesheet', 'stop_other_timesheet', 'create_other_timesheet', 'edit_other_timesheet', 'delete_other_timesheet']; + $timesheetOthers = ['view_other_timesheet', 'start_other_timesheet', 'stop_other_timesheet', 'create_other_timesheet', 'edit_other_timesheet', 'export_other_timesheet', 'delete_other_timesheet']; $profile = ['view_own_profile', 'edit_own_profile', 'password_own_profile', 'preferences_own_profile', 'api-token_own_profile']; $profileOther = ['view_other_profile', 'edit_other_profile', 'delete_other_profile', 'password_other_profile', 'roles_other_profile', 'preferences_other_profile', 'api-token_other_profile']; $user = ['view_user', 'create_user', 'delete_user']; diff --git a/tests/Voter/TimesheetVoterTest.php b/tests/Voter/TimesheetVoterTest.php index 3274b97c..2ed5d6b1 100644 --- a/tests/Voter/TimesheetVoterTest.php +++ b/tests/Voter/TimesheetVoterTest.php @@ -62,6 +62,7 @@ class TimesheetVoterTest extends AbstractVoterTest yield [$timeEntry[0], $timeEntry[1], 'stop', $result]; yield [$timeEntry[0], $timeEntry[1], 'edit', $result]; yield [$timeEntry[0], $timeEntry[1], 'delete', $result]; + yield [$timeEntry[0], $timeEntry[1], 'export', $result]; } $result = VoterInterface::ACCESS_DENIED; @@ -73,6 +74,7 @@ class TimesheetVoterTest extends AbstractVoterTest yield [$timeEntry[0], $timeEntry[1], 'stop', $result]; yield [$timeEntry[0], $timeEntry[1], 'edit', $result]; yield [$timeEntry[0], $timeEntry[1], 'delete', $result]; + yield [$timeEntry[0], $timeEntry[1], 'export', $result]; } } diff --git a/var/docs/permissions.md b/var/docs/permissions.md index 4a5123ff..f9be79b2 100644 --- a/var/docs/permissions.md +++ b/var/docs/permissions.md @@ -71,30 +71,31 @@ The permission-names were chosen to be self-explanatory. In the hope that it wor | stop_own_timesheet | TIMESHEET | | - | | create_own_timesheet | TIMESHEET | X | - | | edit_own_timesheet | TIMESHEET | | - | -| export_own_timesheet | TIMESHEET | | - | +| export_own_timesheet | TIMESHEET | | export your own timesheet in the timesheet panel | | delete_own_timesheet | TIMESHEET | | - | | view_other_timesheet | TIMESHEET_OTHER | | allows access to the complete timesheet view | | start_other_timesheet | TIMESHEET_OTHER | | - | | stop_other_timesheet | TIMESHEET_OTHER | | - | | create_other_timesheet | TIMESHEET_OTHER | | - | | edit_other_timesheet | TIMESHEET_OTHER | | - | +| export_other_timesheet | TIMESHEET | | export timesheet in the timesheet admin panel | | delete_other_timesheet | TIMESHEET_OTHER | | - | | view_rate_own_timesheet | RATE | | - | | edit_rate_own_timesheet | RATE | | - | | view_rate_other_timesheet | RATE_OTHER | | - | | edit_rate_other_timesheet | RATE_OTHER | | - | -| view_own_profile | PROFILE | | Allows access to the own profile view. Without this permission, users cannot access any of their profile settings or passwords ... | -| edit_own_profile | PROFILE | | - | -| delete_own_profile | PROFILE | | - | -| password_own_profile | PROFILE | | - | -| roles_own_profile | PROFILE | | - | -| preferences_own_profile | PROFILE | | - | -| api-token_own_profile | PROFILE | | - | +| view_own_profile | PROFILE | | allows access to the own profile view - without this permission, users cannot access any of their profile settings or passwords ... | +| edit_own_profile | PROFILE | | grants access to edit the own profile | +| delete_own_profile | PROFILE | | grants access to delete the own profile | +| password_own_profile | PROFILE | | grants access to change the own password | +| roles_own_profile | PROFILE | | SECURITY ALERT: grants access to the own roles | +| preferences_own_profile | PROFILE | | grants access to the own preferences | +| api-token_own_profile | PROFILE | | grants access to change the own API token | | view_other_profile | PROFILE_OTHER | | - | | edit_other_profile | PROFILE_OTHER | | - | | delete_other_profile | PROFILE_OTHER | | - | | password_other_profile | PROFILE_OTHER | | allows to change the password for another user | -| roles_other_profile | PROFILE_OTHER | | allows to change roles for other users | +| roles_other_profile | PROFILE_OTHER | | SECURITY ALERT: allows to change roles for other users | | preferences_other_profile | PROFILE_OTHER | | allows to change the preferences for another user | | api-token_other_profile | PROFILE_OTHER | | allows to set the API login token for other users | | hourly-rate_own_profile | - | | allows to edit the own user specific hourly rate |