timesheet export (#317)

* remove autocomplete for begin and end
* fix minute display in timesheet-edit form
* toolbar form label in separate row
* added export action
This commit is contained in:
Kevin Papst
2018-09-24 16:46:10 +02:00
committed by GitHub
parent 7bcd6a6382
commit 98dc38ed99
29 changed files with 422 additions and 95 deletions

View File

@@ -18,7 +18,7 @@ matrix:
- php: 7.2 - php: 7.2
before_install: before_install:
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi - phpenv config-rm xdebug.ini
- composer self-update - composer self-update
install: install:

View File

@@ -51,12 +51,18 @@ $(function() {
$('input[data-datepicker="on"]').daterangepicker({ $('input[data-datepicker="on"]').daterangepicker({
singleDatePicker: true, singleDatePicker: true,
showDropdowns: true, showDropdowns: true,
autoUpdateInput: false,
locale: { locale: {
format: "YYYY-MM-DD", format: "YYYY-MM-DD",
firstDay: 1 firstDay: 1
} }
}); });
$('input[data-datepicker="on"]').on('apply.daterangepicker', function(ev, picker) {
$(this).val(picker.startDate.format('YYYY-MM-DD'));
$(this).trigger("change");
});
$('input[data-datetimepicker="on"]').daterangepicker({ $('input[data-datetimepicker="on"]').daterangepicker({
singleDatePicker: true, singleDatePicker: true,
timePicker: true, timePicker: true,
@@ -69,8 +75,10 @@ $(function() {
firstDay: 1 firstDay: 1
} }
}); });
$('input[data-datetimepicker="on"]').on('apply.daterangepicker', function(ev, picker) { $('input[data-datetimepicker="on"]').on('apply.daterangepicker', function(ev, picker) {
$(this).val(picker.startDate.format('YYYY-MM-DD HH:mm')); $(this).val(picker.startDate.format('YYYY-MM-DD HH:mm'));
$(this).trigger("change");
}); });
/* /*
@@ -98,8 +106,6 @@ $(function() {
// default values // default values
$.kimai.defaults = { $.kimai.defaults = {
baseUrl: '/',
imagePath: '/images',
confirmDelete: 'Really delete?', confirmDelete: 'Really delete?',
alertSuccessAutoHide: 5000 alertSuccessAutoHide: 5000
}; };

View File

@@ -11,6 +11,7 @@
float: right; float: right;
background: transparent; background: transparent;
} }
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */ /* Right sidebar with additional tabs for personal settings and "About Kimai" section */
.control-sidebar { .control-sidebar {
select { select {
@@ -23,7 +24,18 @@
} }
} }
} }
/* The filter form available on most pages above the datatable */
.toolbar form { /* The filter form is available on most pages above the datatable */
font-size: $font-size-base; @media (min-width: 768px) {
.toolbar {
form.navbar-form {
font-size: $font-size-base;
.form-control {
display: inline-block;
width: 100%;
vertical-align: middle;
}
}
}
} }

View File

@@ -43,6 +43,7 @@
<directory suffix=".php">assets/</directory> <directory suffix=".php">assets/</directory>
<directory suffix=".php">bin/</directory> <directory suffix=".php">bin/</directory>
<directory suffix=".php">config/</directory> <directory suffix=".php">config/</directory>
<directory suffix=".php">node_modules/</directory>
<directory suffix=".php">public/</directory> <directory suffix=".php">public/</directory>
<directory suffix=".php">tests/</directory> <directory suffix=".php">tests/</directory>
<directory suffix=".php">translations/</directory> <directory suffix=".php">translations/</directory>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"build/app.js": "/build/app.js?ba9725f5267413a18701", "build/app.js": "/build/app.js?227afb1055d55a1cbd52",
"build/app.css": "/build/app.css?b30340309e980dc272f3ca6d1ff64aa2", "build/app.css": "/build/app.css?6501689dff217d14b179e3049295343e",
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124", "build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd", "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/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",

View File

@@ -60,6 +60,12 @@ class TimesheetController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
/** @var TimesheetQuery $query */ /** @var TimesheetQuery $query */
$query = $form->getData(); $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 */ /* @var $entries Pagerfanta */
@@ -118,7 +124,7 @@ class TimesheetController extends AbstractController
/** /**
* The route to delete an existing entry. * The route to delete an existing entry.
* *
* @Route(path="/{id}/delete", name="admin_timesheet_delete", methods={"GET", "POST"}) * @Route(path="/{id}/delete", defaults={"page": 1}, name="admin_timesheet_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', entry)") * @Security("is_granted('delete', entry)")
* *
* @param Timesheet $entry * @param Timesheet $entry

View File

@@ -42,6 +42,10 @@ class TimesheetController extends AbstractController
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"}) * @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"}) * @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
* @Cache(smaxage="10") * @Cache(smaxage="10")
*
* @param int $page
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/ */
public function indexAction($page, Request $request) public function indexAction($page, Request $request)
{ {
@@ -53,6 +57,12 @@ class TimesheetController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
/** @var TimesheetQuery $query */ /** @var TimesheetQuery $query */
$query = $form->getData(); $query = $form->getData();
if (null !== $query->getBegin()) {
$query->getBegin()->setTime(0, 0, 0);
}
if (null !== $query->getEnd()) {
$query->getEnd()->setTime(23, 59, 59);
}
} }
$query->setUser($this->getUser()); $query->setUser($this->getUser());
@@ -69,7 +79,41 @@ class TimesheetController extends AbstractController
} }
/** /**
* The "main button and flyout" for displaying (and stopping) active entries. * @Route(path="/export", name="timesheet_export", methods={"GET"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function exportAction(Request $request)
{
$query = new TimesheetQuery();
$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);
}
}
$query->setUser($this->getUser());
/* @var $entries Pagerfanta */
$entries = $this->getRepository()->findByQuery($query);
return $this->render('timesheet/export.html.twig', [
'entries' => $entries,
'query' => $query,
]);
}
/**
* The "main button and fly-out" for displaying (and stopping) active entries.
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
@@ -155,7 +199,7 @@ class TimesheetController extends AbstractController
/** /**
* The route to delete an existing entry. * The route to delete an existing entry.
* *
* @Route(path="/{id}/delete", name="timesheet_delete", methods={"GET", "POST"}) * @Route(path="/{id}/delete", defaults={"page": 1}, name="timesheet_delete", methods={"GET", "POST"})
* @Security("is_granted('delete', entry)") * @Security("is_granted('delete', entry)")
* *
* @param Timesheet $entry * @param Timesheet $entry

View File

@@ -92,8 +92,6 @@ trait TimesheetControllerTrait
$record->setEnd($end); $record->setEnd($end);
} }
// TODO validate that end is not before begin
$entityManager = $this->getDoctrine()->getManager(); $entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entry); $entityManager->persist($entry);
$entityManager->flush(); $entityManager->flush();
@@ -170,8 +168,6 @@ trait TimesheetControllerTrait
} }
} }
// TODO validate that end is not before begin
$entityManager = $this->getDoctrine()->getManager(); $entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entry); $entityManager->persist($entry);

View File

@@ -85,6 +85,14 @@ class Activity
*/ */
private $hourlyRate = null; private $hourlyRate = null;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/** /**
* @return Timesheet[] * @return Timesheet[]
*/ */
@@ -113,8 +121,6 @@ class Activity
} }
/** /**
* Set name
*
* @param string $name * @param string $name
* @return Activity * @return Activity
*/ */
@@ -126,8 +132,6 @@ class Activity
} }
/** /**
* Get name
*
* @return string * @return string
*/ */
public function getName() public function getName()
@@ -136,8 +140,6 @@ class Activity
} }
/** /**
* Set comment
*
* @param string $comment * @param string $comment
* @return Activity * @return Activity
*/ */
@@ -149,8 +151,6 @@ class Activity
} }
/** /**
* Get comment
*
* @return string * @return string
*/ */
public function getComment() public function getComment()
@@ -159,10 +159,7 @@ class Activity
} }
/** /**
* Set visible
*
* @param bool $visible * @param bool $visible
*
* @return Activity * @return Activity
*/ */
public function setVisible($visible) public function setVisible($visible)
@@ -173,8 +170,6 @@ class Activity
} }
/** /**
* Get visible
*
* @return bool * @return bool
*/ */
public function getVisible() public function getVisible()
@@ -182,16 +177,6 @@ class Activity
return $this->visible; return $this->visible;
} }
/**
* Get activity id
*
* @return int
*/
public function getId()
{
return $this->id;
}
/** /**
* @return float * @return float
*/ */

View File

@@ -102,8 +102,6 @@ class Project
private $hourlyRate = null; private $hourlyRate = null;
/** /**
* Get projectid
*
* @return int * @return int
*/ */
public function getId() public function getId()
@@ -144,8 +142,6 @@ class Project
} }
/** /**
* Get name
*
* @return string * @return string
*/ */
public function getName() public function getName()
@@ -154,8 +150,6 @@ class Project
} }
/** /**
* Set comment
*
* @param string $comment * @param string $comment
* @return Project * @return Project
*/ */
@@ -167,8 +161,6 @@ class Project
} }
/** /**
* Get comment
*
* @return string * @return string
*/ */
public function getComment() public function getComment()
@@ -177,8 +169,6 @@ class Project
} }
/** /**
* Set visible
*
* @param bool $visible * @param bool $visible
* @return Project * @return Project
*/ */
@@ -190,8 +180,6 @@ class Project
} }
/** /**
* Get visible
*
* @return bool * @return bool
*/ */
public function getVisible() public function getVisible()
@@ -200,8 +188,6 @@ class Project
} }
/** /**
* Set budget
*
* @param float $budget * @param float $budget
* @return Project * @return Project
*/ */
@@ -213,8 +199,6 @@ class Project
} }
/** /**
* Get budget
*
* @return float * @return float
*/ */
public function getBudget() public function getBudget()
@@ -242,7 +226,7 @@ class Project
} }
/** /**
* @return string * @return string|null
*/ */
public function getOrderNumber(): ?string public function getOrderNumber(): ?string
{ {

View File

@@ -44,9 +44,9 @@ class TimesheetEditForm extends AbstractType
'label' => 'label.begin', 'label' => 'label.begin',
'widget' => 'single_text', 'widget' => 'single_text',
'html5' => false, 'html5' => false,
'format' => 'yyyy-MM-dd H:m', 'format' => 'yyyy-MM-dd HH:mm',
'with_seconds' => false, 'with_seconds' => false,
'attr' => ['data-datetimepicker' => 'on'], 'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on'],
]); ]);
} }
@@ -58,9 +58,9 @@ class TimesheetEditForm extends AbstractType
'widget' => 'single_text', 'widget' => 'single_text',
'required' => false, 'required' => false,
'html5' => false, 'html5' => false,
'format' => 'yyyy-MM-dd H:m', 'format' => 'yyyy-MM-dd HH:mm',
'with_seconds' => false, 'with_seconds' => false,
'attr' => ['data-datetimepicker' => 'on'], 'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on'],
]); ]);
} }

View File

@@ -110,7 +110,8 @@ abstract class AbstractToolbarForm extends AbstractType
'widget' => 'single_text', 'widget' => 'single_text',
'html5' => false, 'html5' => false,
'required' => false, 'required' => false,
'attr' => ['data-datepicker' => 'on'], 'format' => 'yyyy-MM-dd',
'attr' => ['autocomplete' => 'off', 'data-datepicker' => 'on'],
]); ]);
} }
@@ -124,7 +125,8 @@ abstract class AbstractToolbarForm extends AbstractType
'widget' => 'single_text', 'widget' => 'single_text',
'html5' => false, 'html5' => false,
'required' => false, 'required' => false,
'attr' => ['data-datepicker' => 'on'], 'format' => 'yyyy-MM-dd',
'attr' => ['autocomplete' => 'off', 'data-datepicker' => 'on'],
]); ]);
} }

View File

@@ -24,6 +24,8 @@ class TimesheetAdminToolbarForm extends TimesheetToolbarForm
$this->addTimesheetStateChoice($builder); $this->addTimesheetStateChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addUserChoice($builder); $this->addUserChoice($builder);
$this->addStartDateChoice($builder);
$this->addEndDateChoice($builder);
$this->addCustomerChoice($builder); $this->addCustomerChoice($builder);
$this->addProjectChoice($builder); $this->addProjectChoice($builder);
$this->addActivityChoice($builder); $this->addActivityChoice($builder);

View File

@@ -26,6 +26,8 @@ class TimesheetToolbarForm extends AbstractToolbarForm
{ {
$this->addTimesheetStateChoice($builder); $this->addTimesheetStateChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addStartDateChoice($builder);
$this->addEndDateChoice($builder);
$this->addCustomerChoice($builder); $this->addCustomerChoice($builder);
$this->addProjectChoice($builder); $this->addProjectChoice($builder);
$this->addActivityChoice($builder); $this->addActivityChoice($builder);

View File

@@ -62,6 +62,8 @@ class Extensions extends \Twig_Extension
'create' => 'far fa-plus-square', 'create' => 'far fa-plus-square',
'dashboard' => 'fas fa-tachometer-alt', 'dashboard' => 'fas fa-tachometer-alt',
'delete' => 'far fa-trash-alt', 'delete' => 'far fa-trash-alt',
'download' => 'fas fa-download',
'duration' => 'far fa-hourglass',
'edit' => 'far fa-edit', 'edit' => 'far fa-edit',
'filter' => 'fas fa-filter', 'filter' => 'fas fa-filter',
'help' => 'far fa-question-circle', 'help' => 'far fa-question-circle',
@@ -69,6 +71,7 @@ class Extensions extends \Twig_Extension
'list' => 'fas fa-list', 'list' => 'fas fa-list',
'logout' => 'fas fa-sign-out-alt', 'logout' => 'fas fa-sign-out-alt',
'manual' => 'fas fa-book', 'manual' => 'fas fa-book',
'money' => 'far fa-money-bill-alt',
'print' => 'fas fa-print', 'print' => 'fas fa-print',
'project' => 'fas fa-project-diagram', 'project' => 'fas fa-project-diagram',
'repeat' => 'fas fa-redo-alt', 'repeat' => 'fas fa-redo-alt',
@@ -80,8 +83,6 @@ class Extensions extends \Twig_Extension
'trash' => 'far fa-trash-alt', 'trash' => 'far fa-trash-alt',
'user' => 'fas fa-user', 'user' => 'fas fa-user',
'visibility' => 'far fa-eye', 'visibility' => 'far fa-eye',
'money' => 'far fa-money-bill-alt',
'duration' => 'far fa-hourglass',
]; ];
/** /**

View File

@@ -94,7 +94,7 @@
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <script> #} {# we do not call parent() as we use a custom built for the frontend assets and don't want the default <script> #}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$.kimai.init({imagePath: '{{ asset('images') }}', confirmDelete: '{{ 'confirm.delete'|trans }}'}); $.kimai.init({confirmDelete: '{{ 'confirm.delete'|trans }}'});
//$.kimai.pauseRecord('li.messages-menu ul.menu li'); //$.kimai.pauseRecord('li.messages-menu ul.menu li');
}); });
</script> </script>

View File

@@ -23,5 +23,3 @@
})|raw }} })|raw }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{#% block form_label %}{% endblock %#}

View File

@@ -37,6 +37,7 @@
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{{ parent() }}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$( "#print-invoice-button" ).click(function() { $( "#print-invoice-button" ).click(function() {

View File

@@ -10,6 +10,9 @@
{% elseif '#modal' in url %} {% elseif '#modal' in url %}
data-toggle="modal" data-target="{{ url }}" data-toggle="modal" data-target="{{ url }}"
{% endif %} {% endif %}
{% if 'onclick:' in url %}
onclick="{{ url|replace({'onclick:': ''}) }}"
{% endif %}
><i class="{{ icon|icon(icon) }}"></i></a> ><i class="{{ icon|icon(icon) }}"></i></a>
{% endfor -%} {% endfor -%}
</div> </div>

View File

@@ -0,0 +1,70 @@
{% import "macros/widgets.html.twig" as widgets %}
{% extends 'invoice/layout.html.twig' %}
{% block invoice %}
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">
{% if query.begin is not empty and query.end is not empty %}
{% if query.begin|date('m') != query.end|date('m') or query.begin|date('Y') != query.end|date('Y') %}
{{ query.begin|date_short }} - {{ query.end|date_short }}:
{% elseif query.end is not empty %}
{{ query.end|month_name|trans }} {{ query.end|date('Y') }}:
{% elseif query.begin is not empty %}
{{ query.begin|month_name|trans }} {{ query.begin|date('Y') }}:
{% endif %}
{% endif %}
{{ widgets.username(query.user) }}
</span>
</h2>
</div>
</div>
<div class="row">
<div class="col-xs-12 table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'label.description'|trans }}</th>
<th>{{ 'label.hours'|trans }}</th>
</tr>
</thead>
<tbody>
{% set timeWorked = 0 %}
{% for entry in entries %}
{% set timeWorked = timeWorked + entry.duration %}
<tr>
<td>{{ entry.begin|date_short }}</td>
<td>
{% if entry.description is not empty %}
<div>
{{ entry.description|desc2html }}
</div>
{% endif %}
<span class="small">
{{ 'label.activity'|trans }}: {{ entry.activity.name }} |
{{ 'label.project'|trans }}: {{ entry.activity.project.name }} |
{{ 'label.customer'|trans }}: {{ entry.activity.project.customer.name }}
</span>
</td>
<td>{{ entry.duration|duration }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th></th>
<th>{{ 'invoice.total_working_time'|trans }}</th>
<th>{{ timeWorked|duration }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
{% endblock %}
{% block print_button %}{% endblock %}

View File

@@ -5,7 +5,7 @@
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %} {% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %} {% block page_subtitle %}{{ 'timesheet.subtitle'|trans }}{% endblock %}
{% block page_actions %}{{ widgets.page_actions({'filter': '#collapseTimesheet', 'visibility': '#modal_timesheet', 'calendar': path('calendar'), 'create': path('timesheet_create')}) }}{% endblock %} {% block page_actions %}{{ widgets.page_actions({'filter': '#collapseTimesheet', 'download': 'onclick:return exportTimesheet()', 'visibility': '#modal_timesheet', 'calendar': path('calendar'), 'create': path('timesheet_create')}) }}{% endblock %}
{% block main_before %} {% block main_before %}
{{ toolbar.toolbar(toolbarForm, 'collapseTimesheet') }} {{ toolbar.toolbar(toolbarForm, 'collapseTimesheet') }}
@@ -81,3 +81,17 @@
{{ tables.data_table_footer(entries, 'timesheet_paginated') }} {{ tables.data_table_footer(entries, 'timesheet_paginated') }}
{% endblock %} {% endblock %}
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
function exportTimesheet() {
var form = $("div.toolbar form.navbar-form");
var prevAction = form.attr('action');
form.attr('target', '_blank').attr('action', '{{ path('timesheet_export') }}');
form.submit();
form.removeAttr('target').attr('action', prevAction);
return false;
}
</script>
{% endblock %}

View File

@@ -9,8 +9,10 @@
namespace App\Tests\Controller\Admin; namespace App\Tests\Controller\Admin;
use App\Entity\Timesheet;
use App\Entity\User; use App\Entity\User;
use App\Tests\Controller\ControllerBaseTest; use App\Tests\Controller\ControllerBaseTest;
use App\Tests\DataFixtures\TimesheetFixtures;
/** /**
* @coversDefaultClass \App\Controller\Admin\TimesheetController * @coversDefaultClass \App\Controller\Admin\TimesheetController
@@ -29,5 +31,111 @@ class TimesheetControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->assertAccessIsGranted($client, '/team/timesheet/'); $this->assertAccessIsGranted($client, '/team/timesheet/');
$this->assertHasDataTable($client); $this->assertHasDataTable($client);
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
$this->assertEquals(3, count($result));
foreach ($result as $item) {
$this->assertEquals('btn btn-default', $item->getAttribute('class'));
$this->assertEquals('i', $item->firstChild->tagName);
}
}
public function testIndexActionWithQuery()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$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();
$client->submit($form, [
'state' => 1,
'user' => 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());
$this->assertHasDataTable($client);
// TODO more assertions
}
public function testCreateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$this->request($client, '/team/timesheet/create');
$this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
$client->submit($form, [
'timesheet_edit_form' => [
'description' => 'Testing is fun!'
]
]);
$this->assertIsRedirect($client, $this->createUrl('/team/timesheet/'));
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertHasFlashSuccess($client);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
/** @var Timesheet $timesheet */
$timesheet = $em->getRepository(Timesheet::class)->find(1);
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
$this->assertNull($timesheet->getEnd());
$this->assertEquals('Testing is fun!', $timesheet->getDescription());
$this->assertEquals(0, $timesheet->getRate());
$this->assertNull($timesheet->getHourlyRate());
$this->assertNull($timesheet->getFixedRate());
}
public function testDeleteActionIsNotAllowedForTeamlead()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate('2017-05-01');
$this->importFixture($em, $fixture);
$this->request($client, '/team/timesheet/1/delete');
$this->assertFalse($client->getResponse()->isSuccessful());
}
public function testDeleteAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate('2017-05-01');
$this->importFixture($em, $fixture);
$this->request($client, '/team/timesheet/1/edit');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->request($client, '/team/timesheet/1/delete');
$this->assertIsRedirect($client, $this->createUrl('/team/timesheet/page/1'));
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertHasFlashSuccess($client);
$this->request($client, '/team/timesheet/1/edit');
$this->assertFalse($client->getResponse()->isSuccessful());
} }
} }

View File

@@ -30,6 +30,76 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->request($client, '/timesheet/'); $this->request($client, '/timesheet/');
$this->assertTrue($client->getResponse()->isSuccessful()); $this->assertTrue($client->getResponse()->isSuccessful());
$this->assertHasDataTable($client); $this->assertHasDataTable($client);
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
$this->assertEquals(5, count($result));
foreach ($result as $item) {
$this->assertEquals('btn btn-default', $item->getAttribute('class'));
$this->assertEquals('i', $item->firstChild->tagName);
}
}
public function testIndexActionWithQuery()
{
$client = $this->getClientForAuthenticatedUser();
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(5);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate(new \DateTime('-10 days'));
$this->importFixture($em, $fixture);
$this->request($client, '/timesheet/');
$this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form.navbar-form')->form();
$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());
$this->assertHasDataTable($client);
// TODO more assertions
}
public function testExportAction()
{
$client = $this->getClientForAuthenticatedUser();
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(5);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate(new \DateTime('-10 days'));
$this->importFixture($em, $fixture);
$this->request($client, '/timesheet/');
$this->assertTrue($client->getResponse()->isSuccessful());
$form = $client->getCrawler()->filter('form.navbar-form')->form();
$form->getFormNode()->setAttribute('action', $this->createUrl('/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(5, count($result));
} }
public function testCreateAction() public function testCreateAction()
@@ -61,6 +131,30 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertNull($timesheet->getFixedRate()); $this->assertNull($timesheet->getFixedRate());
} }
public function testDeleteAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate('2017-05-01');
$this->importFixture($em, $fixture);
$this->request($client, '/timesheet/1/edit');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->request($client, '/timesheet/1/delete');
$this->assertIsRedirect($client, $this->createUrl('/timesheet/page/1'));
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertHasFlashSuccess($client);
$this->request($client, '/timesheet/1/edit');
$this->assertFalse($client->getResponse()->isSuccessful());
}
public function testStartAction() public function testStartAction()
{ {
$client = $this->getClientForAuthenticatedUser(); $client = $this->getClientForAuthenticatedUser();
@@ -184,15 +278,15 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->request($client, '/timesheet/1/edit'); $this->request($client, '/timesheet/1/edit');
$response = $client->getResponse(); $response = $client->getResponse();
$this->assertTrue($response->isSuccessful());
$docuUrl = $this->createUrl('/help/timesheet'); $docuUrl = $this->createUrl('/help/timesheet');
$this->assertTrue($response->isSuccessful());
$this->assertContains( $this->assertContains(
'<a href="' . $docuUrl . '"><i class="far fa-question-circle"></i></a>', '<a href="' . $docuUrl . '"><i class="far fa-question-circle"></i></a>',
$response->getContent(), $response->getContent(),
'Could not find link to documentation' 'Could not find link to documentation'
); );
// TODO more tests // TODO more assertions
} }
} }

View File

@@ -180,7 +180,7 @@ class ExtensionsTest extends TestCase
$icons = [ $icons = [
'user', 'customer', 'project', 'activity', 'admin', 'invoice', 'timesheet', 'dashboard', 'logout', 'trash', 'user', 'customer', 'project', 'activity', 'admin', 'invoice', 'timesheet', 'dashboard', 'logout', 'trash',
'delete', 'repeat', 'edit', 'manual', 'help', 'start', 'start-small', 'stop', 'stop-small', 'filter', 'delete', 'repeat', 'edit', 'manual', 'help', 'start', 'start-small', 'stop', 'stop-small', 'filter',
'create', 'list', 'print', 'visibility', 'calendar', 'money', 'duration', 'create', 'list', 'print', 'visibility', 'calendar', 'money', 'duration', 'download'
]; ];
// test pre-defined icons // test pre-defined icons

View File

@@ -164,7 +164,7 @@ For more details check this [dashboard subscriber](../../src/EventSubscriber/Das
## Adding tabs to the "control sidebar" ## Adding tabs to the "control sidebar"
We use twig globals to render the control sidebar tabs, so adding one is as easy as adding a new config entry: We use the AdminLTE bundle to render the control sidebar tabs, so adding another tab is as easy as adding a new config entry:
```yaml ```yaml
admin_lte: admin_lte:
@@ -179,8 +179,8 @@ admin_lte:
template: sidebar/home.html.twig template: sidebar/home.html.twig
``` ```
You have to define the `icon` ([read more](theme.md)) to be used and either `controller` action or twig `template`. You have to define the `icon` ([read more](theme.md)) to be used and then either `controller` action or twig `template`.
Both follow the default naming syntax and you can link your bundle here instead of the app controller or templates. Both follow the default naming syntax and you can link your bundle here instead of existing application controller or templates.
You should NOT add them in `config/packages/kimai.yaml` but in your own bundle or the `local.yaml` [config](configurations.md), You should NOT add them in `config/packages/kimai.yaml` but in your own bundle or the `local.yaml` [config](configurations.md),
otherwise they might get lost during an update. otherwise they might get lost during an update.

View File

@@ -61,33 +61,34 @@ with a pre-defined list of icon aliases to guarantee a consistent look.
The pre-defined icons aliases are: The pre-defined icons aliases are:
- `user`
- `customer`
- `project`
- `activity` - `activity`
- `admin` - `admin`
- `invoice` - `calendar`
- `timesheet` - `customer`
- `create`
- `dashboard` - `dashboard`
- `logout`
- `trash`
- `delete` - `delete`
- `repeat` - `download`
- `duration`
- `edit` - `edit`
- `manual` - `filter`
- `help` - `help`
- `invoice`
- `list`
- `logout`
- `manual`
- `money`
- `print`
- `project`
- `repeat`
- `start` - `start`
- `start-small` - `start-small`
- `stop` - `stop`
- `stop-small` - `stop-small`
- `filter` - `timesheet`
- `create` - `trash`
- `list` - `user`
- `print`
- `visibility` - `visibility`
- `calendar`
- `money`
- `duration`
Icon aliases can be used by applying the `icon` filter, e.g. Icon aliases can be used by applying the `icon` filter, e.g.

View File

@@ -7,7 +7,7 @@ Encore
// the public path used by the web server to access the previous directory // the public path used by the web server to access the previous directory
.setPublicPath('/build/') .setPublicPath('/build/')
// delete old files before creating them // empty the outputPath directory before each build
.cleanupOutputBeforeBuild() .cleanupOutputBeforeBuild()
// add debug data in development // add debug data in development
@@ -25,9 +25,6 @@ Encore
// show OS notifications when builds finish/fail // show OS notifications when builds finish/fail
.enableBuildNotifications() .enableBuildNotifications()
// empty the outputPath dir before each build
.cleanupOutputBeforeBuild()
// load jquery as Kimai and AdminLTE rely on it // load jquery as Kimai and AdminLTE rely on it
.autoProvidejQuery() .autoProvidejQuery()