0
0
Fork 0
mirror of https://github.com/kevinpapst/kimai2.git synced 2025-04-11 08:01:32 +00:00

Release 2.31 ()

* simplify translation
* bump version
* deprecate translations
* pass date-range as argument to export and timesheet filter URL from monthly overview report
* added class for use in responsive screens
* show technical role name
* simplify multi-update title
* more statistic models
* bump composer packages
This commit is contained in:
Kevin Papst 2025-02-27 17:41:25 +01:00 committed by GitHub
parent 674bf3a6b5
commit 14cdcd3f63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 364 additions and 251 deletions

View file

@ -16,3 +16,9 @@ Perform EACH version specific task between your version and the new one, otherwi
Removed translations:
- `action.edit`: use `edit` instead
- `my.profile`: use `user_profile` instead
- `stats.userAmountToday`: use `` instead
- `stats.userAmountWeek`: use `` instead
- `stats.userAmountMonth`: use `` instead
- `stats.userAmountYear`: use `` instead
- `stats.userAmountTotal`: use `` instead
- `update_multiple`

View file

@ -52,6 +52,13 @@ table.dataTable thead > tr > th.hw-min {
width: 1%;
white-space: normal;
}
@include media-breakpoint-up(sm) {
th.w-sm-min,
td.w-sm-min {
width: 1%;
white-space: nowrap;
}
}
/* If a table column contains ONLY avatar <img> it will collapse, so make it a defined width */
.w-avatar {
width: 40px;

443
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.30.0';
public const VERSION = '2.31.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 23000;
public const VERSION_ID = 23100;
/**
* The software name
*/

View file

@ -38,7 +38,11 @@ final class DashboardController extends AbstractController
*/
private ?array $widgets = null;
public function __construct(private EventDispatcherInterface $eventDispatcher, private WidgetService $service, private BookmarkRepository $repository)
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly WidgetService $service,
private readonly BookmarkRepository $repository
)
{
}

View file

@ -59,11 +59,12 @@ final class ProjectDateRangeController extends AbstractController
$byCustomer[$customer->getId()]['projects'][] = $entry;
}
return $this->render('reporting/project_daterange.html.twig', [
return $this->render('reporting/project/daterange.html.twig', [
'report_title' => 'report_project_daterange',
'entries' => $byCustomer,
'form' => $form->createView(),
'queryEnd' => $dateRange->getEnd(),
'queryBegin' => $begin,
'queryEnd' => $end,
]);
}
}

View file

@ -50,16 +50,21 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
$event->addDivider();
}
$dateRange = '';
if (isset($payload['daterange']) && \is_string($payload['daterange'])) {
$dateRange = $payload['daterange'];
}
if ($this->isGranted('view_activity')) {
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activities', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
}
if ($this->isGranted('view_other_timesheet')) {
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId(), 'daterange' => $dateRange])]);
}
if ($this->isGranted('create_export')) {
$event->addActionToSubmenu('filter', 'export', ['title' => 'export', 'url' => $this->path('export', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId(), 'exported' => 1, 'daterange' => ''])]);
$event->addActionToSubmenu('filter', 'export', ['title' => 'export', 'url' => $this->path('export', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId(), 'exported' => 1, 'daterange' => $dateRange])]);
}
if ($event->hasSubmenu('filter')) {

View file

@ -0,0 +1,54 @@
<?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\Model\Statistic;
final class DateRange
{
/**
* @var array<string, StatisticDate>
*/
private array $days = [];
public function __construct(\DateTimeInterface $begin, \DateTimeInterface $end)
{
if ($end < $begin) {
throw new \InvalidArgumentException('End must be later than begin');
}
$begin = \DateTimeImmutable::createFromInterface($begin);
$begin = $begin->setTime(0, 0, 0);
$end = \DateTimeImmutable::createFromInterface($end);
$end = $end->setTime(23, 59, 59);
do {
$this->days[$begin->format('Y-m-d')] = new StatisticDate($begin);
$begin = $begin->modify('+1 day');
} while ($begin <= $end);
}
/**
* @return array<StatisticDate>
*/
public function getDays(): array
{
return array_values($this->days);
}
public function setDate(StatisticDate $date): void
{
$key = $date->getDate()->format('Y-m-d');
if (!\array_key_exists($key, $this->days)) {
throw new \InvalidArgumentException('Unknown date given');
}
$this->days[$key] = $date;
}
}

View file

@ -14,6 +14,7 @@ final class StatisticDate extends Timesheet
private \DateTimeInterface $date;
private int $billableDuration = 0;
private float $billableRate = 0.00;
private int $amount = 0;
public function __construct(\DateTimeInterface $date)
{
@ -44,4 +45,14 @@ final class StatisticDate extends Timesheet
{
$this->billableRate = $billableRate;
}
public function getAmount(): int
{
return $this->amount;
}
public function setAmount(int $amount): void
{
$this->amount = $amount;
}
}

View file

@ -11,6 +11,7 @@ namespace App\Twig;
use App\Constants;
use App\Entity\EntityWithMetaFields;
use App\Form\Type\DateRangeType;
use App\Utils\Color;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
@ -37,6 +38,7 @@ final class Extensions extends AbstractExtension
public function getFunctions(): array
{
return [
new TwigFunction('date_range', [$this, 'buildDateRange']),
new TwigFunction('report_date', [$this, 'buildReportDate']),
new TwigFunction('class_name', [$this, 'getClassName']),
new TwigFunction('iso_day_by_name', [$this, 'getIsoDayByName']),
@ -90,6 +92,11 @@ final class Extensions extends AbstractExtension
return $dateTime->format(self::REPORT_DATE);
}
public function buildDateRange(\DateTimeInterface $begin, \DateTimeInterface $end): string
{
return $begin->format(self::REPORT_DATE) . DateRangeType::DATE_SPACER . $end->format(self::REPORT_DATE);
}
public function getIsoDayByName(string $weekDay): int
{
$key = array_search(

View file

@ -250,7 +250,7 @@
{% if (route is not empty and entries is not null) or multi_update_form is not null %}
<div class="card-footer d-flex align-items-center">
{% if multi_update_form is not null %}
{{ form_start(multi_update_form, {'attr': {'class': 'multi_update_form', 'style': 'display:none', 'data-question': 'update_multiple'|trans}}) }}
{{ form_start(multi_update_form, {'attr': {'class': 'multi_update_form', 'style': 'display:none', 'data-question': '%action%'}}) }}
{% for formChild in multi_update_form.children %}
{{ form_widget(formChild) }}
{% endfor %}

View file

@ -31,7 +31,7 @@
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<h4 class="card-title">
{{ role.getName()|trans }}
<span ondblclick="navigator.clipboard.writeText('{{ role.getName() }}');" title="{{ role.getName() }}">{{ role.getName()|trans }}</span>
{% if canEditPermissions and (role.name not in system_roles) %}
&nbsp;
<a href="{{ path('admin_user_role_delete', {'role': role.id, 'csrfToken': token}) }}" class="confirmation-link" data-question="confirm.delete">{{ icon('trash') }}</a>

View file

@ -1,6 +1,6 @@
{% macro project(project, view, isTable) %}
{% macro project(project, view, isTable, params) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'project', view, {'project': project, 'token': csrf_token('project.duplicate')}) %}
{% set event = actions(app.user, 'project', view, params|default({})|merge({'project': project, 'token': csrf_token('project.duplicate')})) %}
{% if view == 'index' or view == 'custom' or isTable is not null %}
{{ widgets.table_actions(event.actions) }}
{% else %}

View file

@ -99,7 +99,7 @@
{{ progress.progressbar_budget(entry, currency) }}
{% endif %}
{% elseif name == 'actions' %}
{{ projectActions.project(project, 'custom') }}
{{ projectActions.project(project, 'custom', true, {'daterange': date_range(queryBegin, queryEnd)}) }}
{% endif %}
</td>
{% endfor %}

View file

@ -3,7 +3,7 @@
{% block main %}
{% set formEditTemplate = 'default/_form.html.twig' %}
{% set formOptions = {
'title': 'update_multiple'|trans({'%action%': 'edit'|trans, '%count%': dto.entities|length}),
'title': 'edit'|trans,
'form': form,
'back': path(back),
} %}

View file

@ -43,7 +43,7 @@ abstract class AbstractUserPeriodControllerTestCase extends AbstractControllerBa
public static function getTestData(): array
{
return [
[4, 'duration', 'Working hours total'],
[4, 'duration', 'Total'],
[4, 'rate', 'Total revenue'],
[4, 'internalRate', 'Internal price'],
];
@ -98,6 +98,6 @@ abstract class AbstractUserPeriodControllerTestCase extends AbstractControllerBa
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
self::assertEquals(0, $select->count());
$cell = $client->getCrawler()->filterXPath("//th[contains(@class, 'reportDataTypeTitle')]");
self::assertEquals('Working hours total', $cell->text());
self::assertEquals('Total', $cell->text());
}
}

View file

@ -43,7 +43,7 @@ abstract class AbstractUsersPeriodControllerTestCase extends AbstractControllerB
public static function getTestData(): array
{
return [
['duration', 'Working hours total'],
['duration', 'Total'],
['rate', 'Total revenue'],
['internalRate', 'Internal price'],
];

View file

@ -64,7 +64,7 @@ class ExtensionsTest extends TestCase
public function testGetFunctions(): void
{
$functions = ['report_date', 'class_name', 'iso_day_by_name', 'random_color'];
$functions = ['date_range', 'report_date', 'class_name', 'iso_day_by_name', 'random_color'];
$sut = $this->getSut();
$twigFunctions = $sut->getFunctions();
self::assertCount(\count($functions), $twigFunctions);
@ -221,6 +221,25 @@ sdfsdf' . PHP_EOL . "\n" .
self::assertEquals($expected, $sut->replaceNewline($input, $replacer));
}
public function testReportDate(): void
{
$sut = $this->getSut();
$begin = new \DateTimeImmutable('2025-02-01 17:13:45');
$end = new \DateTimeImmutable('2025-02-25 06:10:00');
self::assertEquals('2025-02-01 - 2025-02-25', $sut->buildDateRange($begin, $end));
}
public function testFormatReportDate(): void
{
$sut = $this->getSut();
$date = new \DateTimeImmutable('2024-07-23 17:13:45');
self::assertEquals('2024-07-23', $sut->formatReportDate($date));
}
public function testGetRandomColor(): void
{
$sut = $this->getSut();

View file

@ -904,27 +904,27 @@
</trans-unit>
<trans-unit id="TdBJBAl" resname="stats.durationToday">
<source>stats.durationToday</source>
<target>Arbeitszeit heute</target>
<target>Heute</target>
</trans-unit>
<trans-unit id="XhKalZH" resname="stats.durationWeek">
<source>stats.durationWeek</source>
<target>Arbeitszeit diese Woche</target>
<target>Diese Woche</target>
</trans-unit>
<trans-unit id="uaOwf_P" resname="stats.durationMonth">
<source>stats.durationMonth</source>
<target>Arbeitszeit diesen Monat</target>
<target>Diesen Monat</target>
</trans-unit>
<trans-unit id="WqF84KR" resname="stats.durationYear">
<source>stats.durationYear</source>
<target>Arbeitszeit dieses Jahr</target>
<target>Dieses Jahr</target>
</trans-unit>
<trans-unit id="xkugSAA" resname="stats.durationFinancialYear">
<source>stats.durationFinancialYear</source>
<target>Arbeitszeit dieses Geschäftsjahr</target>
<target>Geschäftsjahr</target>
</trans-unit>
<trans-unit id="YtvPnl1" resname="stats.durationTotal">
<source>stats.durationTotal</source>
<target>Arbeitszeit gesamt</target>
<target>Gesamt</target>
</trans-unit>
<trans-unit id="EI5gPkp" resname="stats.userDurationToday">
<source>stats.userDurationToday</source>

View file

@ -904,27 +904,27 @@
</trans-unit>
<trans-unit id="TdBJBAl" resname="stats.durationToday">
<source>stats.durationToday</source>
<target>Working hours today</target>
<target>Today</target>
</trans-unit>
<trans-unit id="XhKalZH" resname="stats.durationWeek">
<source>stats.durationWeek</source>
<target>Working hours this week</target>
<target>This week</target>
</trans-unit>
<trans-unit id="uaOwf_P" resname="stats.durationMonth">
<source>stats.durationMonth</source>
<target>Working hours this month</target>
<target>This month</target>
</trans-unit>
<trans-unit id="WqF84KR" resname="stats.durationYear">
<source>stats.durationYear</source>
<target>Working hours this year</target>
<target>This year</target>
</trans-unit>
<trans-unit id="xkugSAA" resname="stats.durationFinancialYear">
<source>stats.durationFinancialYear</source>
<target>Working hours this financial year</target>
<target>Financial year</target>
</trans-unit>
<trans-unit id="YtvPnl1" resname="stats.durationTotal">
<source>stats.durationTotal</source>
<target>Working hours total</target>
<target>Total</target>
</trans-unit>
<trans-unit id="EI5gPkp" resname="stats.userDurationToday">
<source>stats.userDurationToday</source>