small bugfixes (#2655)

* improve report visibility
* fix timesheet duration, which breaks validation
* added contributing section in README
* do not allow to change project for existing activities - fixes #2576
* bump theme version to fix FOSUserBundle dependency
* use entity color instead of inherited one - fixes #2200
This commit is contained in:
Kevin Papst
2021-07-10 00:37:03 +02:00
committed by GitHub
parent 9ddb87a6fd
commit 2583fdf20e
17 changed files with 101 additions and 121 deletions

View File

@@ -25,7 +25,7 @@ It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Do
### Requirements
- PHP 7.2.9 or higher ([PHP 8 not yet](https://github.com/kevinpapst/kimai2/pull/2158))
- PHP 7.2.9 or higher ([PHP 8 support in master](https://github.com/kevinpapst/kimai2/pull/2158))
- MariaDB or MySQL
- Webserver (nginx, Apache)
- Some PHP extensions, see [composer.json](composer.json) or [here](https://www.kimai.org/download/)
@@ -39,6 +39,9 @@ JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagg
authentication via SAML/LDAP/Database, customizable role and team permissions, responsive and ready for your mobile device,
user specific rates, advanced search & filtering, money and time budgets, multiple reports, support for plugins and many more.
Kimai 2 is based on modern frameworks like [Symfony](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json).
## Installation
- [Recommended setup](https://www.kimai.org/documentation/installation.html#recommended-setup) - with Git and Composer
@@ -65,14 +68,22 @@ It is open for changes and input from the community, your [ideas and questions](
> You can upgrade Kimai at any time, you don't need to wait for the next official release.
> The master branch is always deployable, release tags are only snapshots of the current development version.
Release versions will be created on a regular base (approx. one release every 4-8 weeks).
Release versions will be created on a regular base (approx. one release every 2 months).
Every code change, whether it's a new feature or a bugfix, will be done on the master branch.
Kimai is actively developed in my spare time, I put my effort into the software instead of backporting changes.
## Credits
## Contributing
Kimai 2 is developed with modern frameworks like
[Symfony v4](https://github.com/symfony/symfony),
[Doctrine](https://github.com/doctrine/),
[AdminLTEBundle](https://github.com/kevinpapst/AdminLTEBundle/) (based on [AdminLTE theme](https://github.com/almasaeed2010/AdminLTE)) and
[many](composer.json) [more](package.json).
You want to contribute to this repository? This is so great!
The best way to start is to [open a new issue](https://github.com/kevinpapst/kimai2/issues) for bugs or feature requests or a [discussion](https://github.com/kevinpapst/kimai2/discussions) for questions, support and such.
In case you want to contribute, but you wouldn't know how, here are some suggestions:
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms lie Slant, Product Hunt, Softpedia or AlternativeTo, you can tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every little bit helps!
- Answer questions: You know the answer to another user's problem? Share your knowledge!
- Make a feature request: Something can be done better? Something essential missing? Let us know!
- Report bugs
- Contribute: You don't have to be programmer to help. The documentation and translation could use some love as well.
- Sponsor the project
There is one simple rule in our "Code of conduct": Don't be an ass!

View File

@@ -29,7 +29,7 @@
"handcraftedinthealps/rest-routing-bundle": "^1.0",
"jms/metadata": "^2.0",
"jms/serializer-bundle": "^3.9",
"kevinpapst/adminlte-bundle": "dev-master",
"kevinpapst/adminlte-bundle": "^3.5",
"league/csv": "^9.4",
"league/html-to-markdown": "^5.0",
"mpdf/mpdf": "^8.0",

11
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d4fcb30ed5f1db78d4f32f09388ea0b3",
"content-hash": "c031208fe6bb0d88b062cdd70e1b76c6",
"packages": [
{
"name": "beberlei/doctrineextensions",
@@ -2361,7 +2361,7 @@
},
{
"name": "kevinpapst/adminlte-bundle",
"version": "dev-master",
"version": "3.5",
"source": {
"type": "git",
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
@@ -2399,7 +2399,6 @@
"suggest": {
"knplabs/knp-menu-bundle": "Allows easy menu integration"
},
"default-branch": true,
"type": "symfony-bundle",
"autoload": {
"psr-4": {
@@ -2427,7 +2426,7 @@
"description": "Admin theme bundle for Symfony 4+ based on AdminLTE 2.4 with FOSUserBundle support",
"support": {
"issues": "https://github.com/kevinpapst/AdminLTEBundle/issues",
"source": "https://github.com/kevinpapst/AdminLTEBundle/tree/master"
"source": "https://github.com/kevinpapst/AdminLTEBundle/tree/3.5"
},
"funding": [
{
@@ -13685,9 +13684,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"kevinpapst/adminlte-bundle": 20
},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View File

@@ -411,6 +411,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
*/
public function getDuration(): ?int
{
// only auto calculate if manually set duration is null - the result is important for eg. validations
if ($this->duration === null && $this->begin !== null && $this->end !== null) {
return $this->end->getTimestamp() - $this->begin->getTimestamp();
}
return $this->duration;
}

View File

@@ -72,7 +72,7 @@ class ProjectSubscriber extends AbstractActionsSubscriber
$event->addDelete($this->path('admin_project_delete', ['id' => $project->getId()]));
}
if ($this->isGranted('view_reporting') && $this->isGranted('details_project')) {
if ($project->isVisible() && $this->isGranted('view_reporting') && $this->isGranted('details_project')) {
$event->addAction('report_project_details', ['url' => $this->path('report_project_details', ['project' => $project->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']);
}
}

View File

@@ -36,7 +36,7 @@ class ActivityEditForm extends AbstractType
{
$project = null;
$customer = null;
$id = null;
$new = true;
if (isset($options['data'])) {
/** @var Activity $entry */
@@ -48,7 +48,7 @@ class ActivityEditForm extends AbstractType
$options['currency'] = $customer->getCurrency();
}
$id = $entry->getId();
$new = $entry->getId() === null;
}
$builder
@@ -64,55 +64,57 @@ class ActivityEditForm extends AbstractType
])
;
if ($options['customer']) {
if ($new) {
if ($options['customer']) {
$builder
->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
$query = new CustomerFormTypeQuery($customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
'data' => $customer ? $customer : null,
'required' => false,
'mapped' => false,
'project_enabled' => true,
]);
}
$builder
->add('customer', CustomerType::class, [
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
$query = new CustomerFormTypeQuery($customer);
$query->setUser($builder->getOption('user'));
return $repo->getQueryBuilderForFormType($query);
},
'data' => $customer ? $customer : null,
->add('project', ProjectType::class, [
'required' => false,
'mapped' => false,
'project_enabled' => true,
]);
}
$builder
->add('project', ProjectType::class, [
'required' => false,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setIgnoreDate(true);
return $repo->getQueryBuilderForFormType($query);
},
]);
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project) {
$data = $event->getData();
if (!isset($data['customer']) || empty($data['customer'])) {
return;
}
$event->getForm()->add('project', ProjectType::class, [
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $project) {
$query = new ProjectFormTypeQuery($project, $data['customer']);
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setIgnoreDate(true);
return $repo->getQueryBuilderForFormType($query);
},
]);
}
);
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project) {
$data = $event->getData();
if (!isset($data['customer']) || empty($data['customer'])) {
return;
}
$event->getForm()->add('project', ProjectType::class, [
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $project) {
$query = new ProjectFormTypeQuery($project, $data['customer']);
$query->setUser($builder->getOption('user'));
$query->setIgnoreDate(true);
return $repo->getQueryBuilderForFormType($query);
},
]);
}
);
}
$this->addCommonFields($builder, $options);
}

View File

@@ -32,6 +32,7 @@ class ProjectDetailsForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('project', ProjectType::class, [
'ignore_date' => true,
'required' => false,
'label' => false,
'width' => false,

View File

@@ -18,7 +18,7 @@
{% import "project/actions.html.twig" as projectActions %}
{% block box_attributes %}id="activity_details_box"{% endblock %}
{% block box_title %}
{{ widgets.label_activity(activity) }}
{{ widgets.label_name(activity.name, activity.color) }}
{% endblock %}
{% block box_tools %}
{% if can_edit %}

View File

@@ -22,8 +22,12 @@
</div>
</div>
{{ form_row(form.comment) }}
{{ form_row(form.customer) }}
{{ form_row(form.project) }}
{% if form.customer is defined %}
{{ form_row(form.customer) }}
{% endif %}
{% if form.project is defined %}
{{ form_row(form.project) }}
{% endif %}
{% if form.budget is defined %}
<div class="row">
<div class="col-md-6">

View File

@@ -2,7 +2,7 @@
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="{{ id }}"{% endblock %}
{% block box_title %}
{{ 'rates.title'|trans }}
{{ 'label.hourlyRate'|trans }}
{% endblock %}
{% block box_tools %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm" data-href="{{ create_url }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>

View File

@@ -18,7 +18,7 @@
{% endif %}
{% endblock %}
{% block box_title %}
{{ widgets.label_project(project) }}
{{ widgets.label_name(project.name, project.color) }}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body %}

View File

@@ -260,7 +260,6 @@
<div class="nav-tabs-custom">
<ul class="nav nav-tabs pull-right">
<li><a data-chart="{{ chartPrefix }}User" href="#user-chart" data-toggle="tab">{{ 'label.user'|trans }}</a></li>
<li><a data-chart="{{ chartPrefix }}Activity" href="#activity-chart" data-toggle="tab">{{ 'label.activity'|trans }}</a></li>
{% if view_revenue_tab %}
@@ -268,14 +267,14 @@
{% endif %}
<li><a data-chart="{{ chartPrefix }}Duration" href="#time-chart" data-toggle="tab">{{ 'stats.workingTime'|trans }}</a></li>
<li class="active"><a href="#details-chart" data-toggle="tab">{{ 'report_project_details'|trans({}, 'reporting') }}</a></li>
<li class="pull-left header">
<li class="pull-left header hidden-xs">
{{ widgets.label_project(project, {'inherit': false}) }}
</li>
</ul>
<div class="tab-content no-padding">
<div class="chart tab-pane active" id="details-chart">
<div class="row">
<div class="col-xs-6">
<div class="col-xs-12 col-md-6">
<table class="table table-hover dataTable">
<tr {{ widgets.customer_row_attr(entry.project.customer) }}>
<th class="w-min">
@@ -285,33 +284,21 @@
{{ widgets.label_customer(entry.project.customer) }}
</td>
</tr>
<tr>
<th class="w-min">
{{ 'stats.durationToday'|trans }}
</th>
<td>{{ entry.durationDay|duration }}</td>
</tr>
<tr>
<th class="w-min">
{{ 'stats.durationWeek'|trans }}
</th>
<td>{{ entry.durationWeek|duration }}</td>
</tr>
<tr>
<th class="w-min">
{{ 'stats.durationMonth'|trans }}
</th>
<td>{{ entry.durationMonth|duration }}</td>
</tr>
<tr>
<th class="w-min">
{{ 'stats.durationTotal'|trans }}
</th>
<td>{{ entry.durationTotal|duration }}</td>
</tr>
<tr>
<th class="w-min">
{{ 'stats.amountTotal'|trans }}
</th>
<td>{{ entry.rateTotal|money(currency) }}</td>
</tr>
</table>
</div>
<div class="col-xs-6">
<div class="col-xs-12 col-md-6">
<table class="table table-hover dataTable">
<tr>
<th class="w-min">
@@ -353,26 +340,6 @@
{% endif %}
</td>
</tr>
{% if project.start is not null %}
<tr>
<th class="w-min">
{{ 'label.project_start'|trans }}
</th>
<td>
{{ project.start|date_short }}
</td>
</tr>
{% endif %}
{% if project.end is not null %}
<tr>
<th class="w-min">
{{ 'label.project_end'|trans }}
</th>
<td>
{{ project.end|date_short }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>

View File

@@ -291,7 +291,6 @@ class ActivityControllerTest extends APIControllerBaseTest
$data = [
'name' => 'foo',
'comment' => '',
'project' => 1,
'visible' => true,
'budget' => '999',
'timeBudget' => '7200',
@@ -332,15 +331,14 @@ class ActivityControllerTest extends APIControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'project' => 255,
'visible' => true
'name' => 'foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo',
'visible' => false
];
$this->request($client, '/api/activities/1', 'PATCH', [], json_encode($data));
$response = $client->getResponse();
$this->assertEquals(400, $response->getStatusCode());
$this->assertApiCallValidationError($response, ['project']);
$this->assertApiCallValidationError($response, ['name']);
}
public function testMetaActionThrowsNotFound()

View File

@@ -189,9 +189,6 @@ class ActivityControllerTest extends ControllerBaseTest
$this->request($client, '/admin/activity/' . $id . '/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 testCreateActionShowsMetaFields()
@@ -213,15 +210,13 @@ class ActivityControllerTest extends ControllerBaseTest
$form = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
$this->assertEquals('Test', $form->get('activity_edit_form[name]')->getValue());
$client->submit($form, [
'activity_edit_form' => ['name' => 'Test 2', 'customer' => 1, 'project' => '1']
'activity_edit_form' => ['name' => 'Test 2']
]);
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/1/details'));
$client->followRedirect();
$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());
$this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue());
$this->assertEquals('1', $editForm->get('activity_edit_form[project]')->getValue());
}
public function testEditActionForGlobalActivity()

View File

@@ -102,7 +102,7 @@
</trans-unit>
<trans-unit id="rates.empty">
<source>rates.empty</source>
<target>Es wurden noch keine Gebühren hinterlegt.</target>
<target>Es wurden noch keine Stundensätze hinterlegt.</target>
</trans-unit>
<trans-unit id="rates.title">
<source>rates.title</source>

View File

@@ -984,7 +984,7 @@
</trans-unit>
<trans-unit id="rates.empty">
<source>rates.empty</source>
<target>Es wurden noch keine Gebühren hinterlegt.</target>
<target>Es wurden noch keine Stundensätze hinterlegt.</target>
</trans-unit>
<trans-unit id="attachments">
<source>attachments</source>

View File

@@ -102,7 +102,7 @@
</trans-unit>
<trans-unit id="rates.empty">
<source>rates.empty</source>
<target>No fees have been configured yet.</target>
<target>No hourly rates have yet been configured.</target>
</trans-unit>
<trans-unit id="rates.title">
<source>rates.title</source>