random improvements (#5382)

* remove permission check, as own timesheets should always be visible
* new methods to create datetime
* allow access to user roles in javascript
* support class for dropdown actions
* allow to edit internal rate
* support human readable duration in export via user configuration
* allow to order timesheet listing by user, exported and billable field
* bump codecov action
This commit is contained in:
Kevin Papst
2025-03-13 18:00:50 +01:00
committed by GitHub
parent 934fdeb107
commit 2a75cd6230
26 changed files with 271 additions and 36 deletions

View File

@@ -104,7 +104,7 @@ jobs:
- name: Upload code coverage - name: Upload code coverage
if: matrix.php == '8.2' if: matrix.php == '8.2'
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml files: ./coverage.xml

View File

@@ -49,4 +49,11 @@ export default class KimaiUser extends KimaiPlugin {
return this.user.superAdmin; return this.user.superAdmin;
} }
/**
* @returns {array}
*/
getRoles() {
return this.user.roles;
}
} }

View File

@@ -16,7 +16,6 @@ export default class KimaiPaginatedBoxWidget {
constructor(boxId) { constructor(boxId) {
this.selector = boxId; this.selector = boxId;
const widget = document.querySelector(this.selector); const widget = document.querySelector(this.selector);
this.href = widget.dataset['href'];
if (widget.dataset['reload'] !== undefined) { if (widget.dataset['reload'] !== undefined) {
this.events = widget.dataset['reload'].split(' '); this.events = widget.dataset['reload'].split(' ');
@@ -93,7 +92,7 @@ export default class KimaiPaginatedBoxWidget {
if (node.tagName !== undefined && node.tagName === 'SCRIPT') { if (node.tagName !== undefined && node.tagName === 'SCRIPT') {
const script = document.createElement('script'); const script = document.createElement('script');
script.text = node.innerHTML; script.text = node.innerHTML;
node.parentNode.replaceChild(script, node ); node.parentNode.replaceChild(script, node);
} else { } else {
for (const child of node.childNodes) { for (const child of node.childNodes) {
this._makeScriptExecutable(child); this._makeScriptExecutable(child);

View File

@@ -106,11 +106,11 @@ abstract class TimesheetAbstractController extends AbstractController
} }
if ($canSeeUsername) { if ($canSeeUsername) {
$table->addColumn('username', ['class' => 'd-none d-md-table-cell', 'orderBy' => false]); $table->addColumn('username', ['class' => 'd-none d-md-table-cell', 'orderBy' => 'user']);
} }
$table->addColumn('billable', ['class' => 'text-center d-none w-min', 'orderBy' => false]); $table->addColumn('billable', ['class' => 'text-center d-none w-min']);
$table->addColumn('exported', ['class' => 'text-center d-none w-min', 'orderBy' => false]); $table->addColumn('exported', ['class' => 'text-center d-none w-min']);
$table->addColumn('actions', ['class' => 'actions']); $table->addColumn('actions', ['class' => 'actions']);
$page = $this->createPageSetup(); $page = $this->createPageSetup();

View File

@@ -45,6 +45,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
public function indexAction(int $page, Request $request): Response public function indexAction(int $page, Request $request): Response
{ {
$query = $this->createDefaultQuery(); $query = $this->createDefaultQuery();
$query->addAllowedOrderColumns('user');
$query->setPage($page); $query->setPage($page);
return $this->index($query, $request, 'admin_timesheet', 'admin_timesheet_paginated', TimesheetMetaDisplayEvent::TEAM_TIMESHEET); return $this->index($query, $request, 'admin_timesheet', 'admin_timesheet_paginated', TimesheetMetaDisplayEvent::TEAM_TIMESHEET);

View File

@@ -11,6 +11,7 @@ namespace App\Export\Base;
use App\Entity\ExportableItem; use App\Entity\ExportableItem;
use App\Entity\MetaTableTypeInterface; use App\Entity\MetaTableTypeInterface;
use App\Entity\User;
use App\Event\ActivityMetaDisplayEvent; use App\Event\ActivityMetaDisplayEvent;
use App\Event\CustomerMetaDisplayEvent; use App\Event\CustomerMetaDisplayEvent;
use App\Event\MetaDisplayEventInterface; use App\Event\MetaDisplayEventInterface;
@@ -22,6 +23,7 @@ use App\Export\Package\CellFormatter\BooleanFormatter;
use App\Export\Package\CellFormatter\CellFormatterInterface; use App\Export\Package\CellFormatter\CellFormatterInterface;
use App\Export\Package\CellFormatter\DateFormatter; use App\Export\Package\CellFormatter\DateFormatter;
use App\Export\Package\CellFormatter\DefaultFormatter; use App\Export\Package\CellFormatter\DefaultFormatter;
use App\Export\Package\CellFormatter\DurationDecimalFormatter;
use App\Export\Package\CellFormatter\DurationFormatter; use App\Export\Package\CellFormatter\DurationFormatter;
use App\Export\Package\CellFormatter\RateFormatter; use App\Export\Package\CellFormatter\RateFormatter;
use App\Export\Package\CellFormatter\TextFormatter; use App\Export\Package\CellFormatter\TextFormatter;
@@ -130,6 +132,7 @@ final class SpreadsheetRenderer
'date' => new DateFormatter(), 'date' => new DateFormatter(),
'time' => new TimeFormatter(), 'time' => new TimeFormatter(),
'duration' => new DurationFormatter(), 'duration' => new DurationFormatter(),
'duration_decimal' => new DurationDecimalFormatter(),
default => new DefaultFormatter() default => new DefaultFormatter()
}; };
} }
@@ -141,12 +144,17 @@ final class SpreadsheetRenderer
{ {
$showRates = $this->isRenderRate($query); $showRates = $this->isRenderRate($query);
$durationFormatter = 'duration';
if (($user = $this->voter->getUser()) instanceof User) {
$durationFormatter = $user->isExportDecimal() ? 'duration_decimal' : 'duration';
}
$columns = []; $columns = [];
$columns[] = (new Column('date', $this->getFormatter('date')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin()); $columns[] = (new Column('date', $this->getFormatter('date')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin());
$columns[] = (new Column('begin', $this->getFormatter('time')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin())->withColumnWidth(ColumnWidth::SMALL); $columns[] = (new Column('begin', $this->getFormatter('time')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin())->withColumnWidth(ColumnWidth::SMALL);
$columns[] = (new Column('end', $this->getFormatter('time')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getEnd())->withColumnWidth(ColumnWidth::SMALL); $columns[] = (new Column('end', $this->getFormatter('time')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getEnd())->withColumnWidth(ColumnWidth::SMALL);
$columns[] = (new Column('duration', $this->getFormatter('duration')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getDuration())->withColumnWidth(ColumnWidth::SMALL); $columns[] = (new Column('duration', $this->getFormatter($durationFormatter)))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getDuration())->withColumnWidth(ColumnWidth::SMALL);
if ($showRates) { if ($showRates) {
$columns[] = (new Column('currency', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getCurrency())->withColumnWidth(ColumnWidth::SMALL); $columns[] = (new Column('currency', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getCurrency())->withColumnWidth(ColumnWidth::SMALL);

View File

@@ -0,0 +1,31 @@
<?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\Export\Package\CellFormatter;
use App\Utils\Duration;
final class DurationDecimalFormatter implements CellFormatterInterface
{
private Duration $duration;
public function __construct()
{
$this->duration = new Duration();
}
public function formatValue(mixed $value): mixed
{
if (is_numeric($value)) {
return $this->duration->formatDecimal((int) $value);
}
return 0.0;
}
}

View File

@@ -9,14 +9,23 @@
namespace App\Export\Package\CellFormatter; namespace App\Export\Package\CellFormatter;
use App\Utils\Duration;
final class DurationFormatter implements CellFormatterInterface final class DurationFormatter implements CellFormatterInterface
{ {
private Duration $duration;
public function __construct()
{
$this->duration = new Duration();
}
public function formatValue(mixed $value): mixed public function formatValue(mixed $value): mixed
{ {
if (is_numeric($value)) { if (is_numeric($value)) {
return (float) number_format($value / 3600, 2, '.', ''); return $this->duration->format((int) $value);
} }
return 0.0; return $this->duration->format(0);
} }
} }

View File

@@ -18,6 +18,7 @@ use App\Form\Type\DescriptionType;
use App\Form\Type\DurationType; use App\Form\Type\DurationType;
use App\Form\Type\FixedRateType; use App\Form\Type\FixedRateType;
use App\Form\Type\HourlyRateType; use App\Form\Type\HourlyRateType;
use App\Form\Type\InternalRateType;
use App\Form\Type\MetaFieldsCollectionType; use App\Form\Type\MetaFieldsCollectionType;
use App\Form\Type\TagsType; use App\Form\Type\TagsType;
use App\Form\Type\TimePickerType; use App\Form\Type\TimePickerType;
@@ -365,6 +366,9 @@ class TimesheetEditForm extends AbstractType
]) ])
->add('hourlyRate', HourlyRateType::class, [ ->add('hourlyRate', HourlyRateType::class, [
'currency' => $currency, 'currency' => $currency,
])
->add('internalRate', InternalRateType::class, [
'currency' => $currency,
]); ]);
} }

View File

@@ -44,8 +44,12 @@ final class TimesheetToolbarForm extends AbstractType
$this->addExportStateChoice($builder); $this->addExportStateChoice($builder);
$this->addPageSizeChoice($builder); $this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder); $this->addHiddenPagination($builder);
$this->addOrder($builder);
$this->addOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED); $query = $options['data'];
if ($query instanceof TimesheetQuery) {
$this->addOrder($builder);
$this->addOrderBy($builder, $query->getAllowedOrderColumns());
}
} }
public function configureOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void

View File

@@ -0,0 +1,38 @@
<?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\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to set the internal rate.
*/
final class InternalRateType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// documentation is for NelmioApiDocBundle
'documentation' => [
'type' => 'number',
'description' => 'Internal (hourly) rate',
],
'required' => false,
'label' => 'internalRate',
]);
}
public function getParent(): string
{
return MoneyType::class;
}
}

View File

@@ -44,6 +44,10 @@ class BaseQuery
* @var array<string, string> * @var array<string, string>
*/ */
private array $orderGroups = []; private array $orderGroups = [];
/**
* @var array<string>
*/
private array $allowedOrderColumns = [];
private ?User $currentUser = null; private ?User $currentUser = null;
/** /**
* @var array<Team> * @var array<Team>
@@ -428,4 +432,25 @@ class BaseQuery
{ {
return $this->isApiCall; return $this->isApiCall;
} }
/**
* @return string[]
*/
public function getAllowedOrderColumns(): array
{
return $this->allowedOrderColumns;
}
/**
* @param array<string> $allowedOrderColumns
*/
public function setAllowedOrderColumns(array $allowedOrderColumns): void
{
$this->allowedOrderColumns = $allowedOrderColumns;
}
public function addAllowedOrderColumns(string $allowedOrderColumn): void
{
$this->allowedOrderColumns[] = $allowedOrderColumn;
}
} }

View File

@@ -25,7 +25,10 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface, DateRan
public const STATE_EXPORTED = 4; public const STATE_EXPORTED = 4;
public const STATE_NOT_EXPORTED = 5; public const STATE_NOT_EXPORTED = 5;
public const TIMESHEET_ORDER_ALLOWED = ['begin', 'end', 'duration', 'rate', 'hourlyRate', 'customer', 'project', 'activity', 'description']; /**
* @deprecated since 2.31.0
*/
public const TIMESHEET_ORDER_ALLOWED = ['begin', 'end', 'duration', 'rate', 'hourlyRate', 'customer', 'project', 'activity', 'description', 'billable', 'exported'];
private ?User $timesheetUser = null; private ?User $timesheetUser = null;
/** @var array<Activity> */ /** @var array<Activity> */
@@ -61,6 +64,7 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface, DateRan
'users' => [], 'users' => [],
'activities' => [], 'activities' => [],
]); ]);
$this->setAllowedOrderColumns(self::TIMESHEET_ORDER_ALLOWED); // @phpstan-ignore-line
} }
public function addQueryHint(TimesheetQueryHint $hint): void public function addQueryHint(TimesheetQueryHint $hint): void

View File

@@ -730,11 +730,6 @@ class TimesheetRepository extends EntityRepository
->setParameter('begin', \DateTimeImmutable::createFromInterface($startFrom), Types::DATETIME_IMMUTABLE); ->setParameter('begin', \DateTimeImmutable::createFromInterface($startFrom), Types::DATETIME_IMMUTABLE);
} }
$qb->join('t.project', 'p');
$qb->join('p.customer', 'c');
$this->addPermissionCriteria($qb, $user);
$results = $qb->getQuery()->getScalarResult(); $results = $qb->getQuery()->getScalarResult();
if (empty($results)) { if (empty($results)) {

View File

@@ -169,6 +169,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
public function refreshUser(UserInterface $user): User public function refreshUser(UserInterface $user): User
{ {
// TODO 3.0 add a last_updated field to user and ONLY load this for comparison.
// TODO then only execute the below code if last_updated != session.last_updated
return $this->loadUserByIdentifier($user->getUserIdentifier()); return $this->loadUserByIdentifier($user->getUserIdentifier());
} }

View File

@@ -69,6 +69,19 @@ final class DateTimeFactory
return DateTime::createFromInterface($date); return DateTime::createFromInterface($date);
} }
private function createDate(DateTimeInterface|string|null $date = null): \DateTimeImmutable
{
if ($date === null) {
$date = 'now';
}
if (\is_string($date)) {
return $this->create($date);
}
return \DateTimeImmutable::createFromInterface($date);
}
public function getStartOfWeek(DateTimeInterface|string|null $date = null): DateTime public function getStartOfWeek(DateTimeInterface|string|null $date = null): DateTime
{ {
$date = $this->getDate($date); $date = $this->getDate($date);
@@ -139,6 +152,20 @@ final class DateTimeFactory
return new \DateTimeImmutable($datetime, $this->getTimezone()); return new \DateTimeImmutable($datetime, $this->getTimezone());
} }
public function createStartOfDay(DateTimeInterface|string|null $date = null): \DateTimeImmutable
{
$date = $this->createDate($date);
return $date->modify('00:00:00');
}
public function createEndOfDay(DateTimeInterface|string|null $date = null): \DateTimeImmutable
{
$date = $this->createDate($date);
return $date->modify('23:59:59');
}
/** /**
* @param string $format * @param string $format
* @param null|string $datetime * @param null|string $datetime

View File

@@ -191,6 +191,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
$admin = false; $admin = false;
$superAdmin = false; $superAdmin = false;
$timezone = date_default_timezone_get(); $timezone = date_default_timezone_get();
$roles = [];
if ($user !== null) { if ($user !== null) {
$browserTitle = (bool) $user->getPreferenceValue('update_browser_title'); $browserTitle = (bool) $user->getPreferenceValue('update_browser_title');
@@ -200,6 +201,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
$admin = $user->isAdmin(); $admin = $user->isAdmin();
$superAdmin = $user->isSuperAdmin(); $superAdmin = $user->isSuperAdmin();
$timezone = $user->getTimezone(); $timezone = $user->getTimezone();
$roles = $user->getRoles();
} }
$language ??= $this->locale ?? User::DEFAULT_LANGUAGE; $language ??= $this->locale ?? User::DEFAULT_LANGUAGE;
@@ -213,7 +215,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
'twentyFourHours' => $this->localeService->is24Hour($this->locale), 'twentyFourHours' => $this->localeService->is24Hour($this->locale),
'updateBrowserTitle' => $browserTitle, 'updateBrowserTitle' => $browserTitle,
'timezone' => $timezone, 'timezone' => $timezone,
'user' => ['id' => $id, 'name' => $name, 'admin' => $admin, 'superAdmin' => $superAdmin], 'user' => ['id' => $id, 'name' => $name, 'admin' => $admin, 'superAdmin' => $superAdmin, 'roles' => $roles],
]; ];
} }

View File

@@ -19,12 +19,17 @@ final class Duration
public const FORMAT_DECIMAL = 'decimal'; public const FORMAT_DECIMAL = 'decimal';
public const FORMAT_DEFAULT = '%h:%m'; public const FORMAT_DEFAULT = '%h:%m';
public function formatDecimal(?int $seconds): float
{
if ($seconds === null || $seconds === 0) {
return 0.0;
}
return (float) number_format($seconds / 3600, 2, '.', '');
}
/** /**
* Transforms seconds into a duration string. * Transforms seconds into a duration string.
*
* @param int|null $seconds
* @param string $format
* @return string|null
*/ */
public function format(?int $seconds, string $format = self::FORMAT_DEFAULT): ?string public function format(?int $seconds, string $format = self::FORMAT_DEFAULT): ?string
{ {

View File

@@ -40,7 +40,7 @@ abstract class AbstractWidget implements WidgetInterface
public function getWidth(): int public function getWidth(): int
{ {
return WidgetInterface::WIDTH_SMALL; return WidgetInterface::WIDTH_HALF;
} }
public function getPermissions(): array public function getPermissions(): array

View File

@@ -61,7 +61,7 @@
{# what to do here ? #} {# what to do here ? #}
{% else %} {% else %}
{% if values.children is defined and values.children|length > 0 %} {% if values.children is defined and values.children|length > 0 %}
<div class="dropdown"> <div class="dropdown{% if values.class is defined %} {{ values.class }}{% endif %}">
<button type="button" class="btn {{ btnClasses }} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn {{ btnClasses }} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans }}{% endif %} {{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans }}{% endif %}
</button> </button>

View File

@@ -1,10 +1,8 @@
{% embed '@theme/embeds/card.html.twig' with {'project': project, 'activities': activities, 'page': page} %} {% embed '@theme/embeds/card.html.twig' with {'project': project, 'activities': activities, 'page': page, 'id': 'activity_list_box'} %}
{% import "activity/actions.html.twig" as actions %} {% import "activity/actions.html.twig" as actions %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'activities'|trans }}{% endblock %} {% block box_title %}{{ 'activities'|trans }}{% endblock %}
{% block box_attributes %} {% block box_attributes %} data-reload="kimai.activityUpdate kimai.activityDelete" {% endblock %}
id="activity_list_box" data-reload="kimai.activityUpdate kimai.activityDelete"
{% endblock %}
{% block box_tools %} {% block box_tools %}
{%- if project.visible and project.customer.visible and is_granted('create_activity') -%} {%- if project.visible and project.customer.visible and is_granted('create_activity') -%}
{{ widgets.card_tool_button('create', {'class': 'modal-ajax-form open-edit', 'title': 'create', 'url': path('admin_activity_create_with_project', {'project': project.id})}) }} {{ widgets.card_tool_button('create', {'class': 'modal-ajax-form open-edit', 'title': 'create', 'url': path('admin_activity_create_with_project', {'project': project.id})}) }}

View File

@@ -76,7 +76,7 @@
{% if form.metaFields is defined and form.metaFields is not empty %} {% if form.metaFields is defined and form.metaFields is not empty %}
{{ form_row(form.metaFields) }} {{ form_row(form.metaFields) }}
{% endif %} {% endif %}
{% if form.fixedRate is defined or form.hourlyRate is defined or form.billable is defined or form.billableMode is defined or form.exported is defined %} {% if form.fixedRate is defined or form.hourlyRate is defined or form.internalRate is defined or form.billable is defined or form.billableMode is defined or form.exported is defined %}
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'timesheet_extended_settings'} %} {% embed '@theme/embeds/collapsible.html.twig' with {id: 'timesheet_extended_settings'} %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% block title %}{{ 'extended_settings'|trans }}{% endblock %} {% block title %}{{ 'extended_settings'|trans }}{% endblock %}
@@ -87,6 +87,9 @@
{% if form.hourlyRate is defined %} {% if form.hourlyRate is defined %}
{{ form_row(form.hourlyRate, {'row_attr': {'class': 'mb-3 ' ~ form.vars.name ~ '_row_' ~ form.hourlyRate.vars.name}}) }} {{ form_row(form.hourlyRate, {'row_attr': {'class': 'mb-3 ' ~ form.vars.name ~ '_row_' ~ form.hourlyRate.vars.name}}) }}
{% endif %} {% endif %}
{% if form.internalRate is defined %}
{{ form_row(form.internalRate, {'row_attr': {'class': 'mb-3 ' ~ form.vars.name ~ '_row_' ~ form.internalRate.vars.name}}) }}
{% endif %}
{% if form.billable is defined %} {% if form.billable is defined %}
{{ form_row(form.billable, {'row_attr': {'class': 'mb-3 ' ~ form.vars.name ~ '_row_' ~ form.billable.vars.name}}) }} {{ form_row(form.billable, {'row_attr': {'class': 'mb-3 ' ~ form.vars.name ~ '_row_' ~ form.billable.vars.name}}) }}
{% elseif form.billableMode is defined %} {% elseif form.billableMode is defined %}

View File

@@ -102,7 +102,8 @@ class CsvRendererTest extends AbstractRendererTestCase
'2019-06-16', '2019-06-16',
'12:00', '12:00',
'12:06', '12:06',
'0.11', '0:06',
//'0.11',
'EUR', 'EUR',
'0', '0',
'0', '0',
@@ -134,7 +135,8 @@ class CsvRendererTest extends AbstractRendererTestCase
'2019-06-16', '2019-06-16',
'12:00', '12:00',
'12:06', '12:06',
'0.11', '0:06',
//'0.11',
'EUR', 'EUR',
'0', '0',
'0', '0',

View File

@@ -0,0 +1,68 @@
<?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\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DurationDecimalFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DurationDecimalFormatter
*/
class DurationDecimalFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedDurationForNumericValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue(7200);
self::assertEquals(2.00, $result);
}
public function testFormatValueReturnsZeroForNonNumericValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue('not a number');
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedDurationForFloatValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue(4500.5);
self::assertEquals(1.25, $result);
}
public function testFormatValueReturnsFormattedDurationForStringValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue('4500.5');
self::assertEquals(1.25, $result);
}
public function testFormatValueReturnsZeroForNullValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue(null);
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedDurationForNegativeValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue(-3600);
self::assertEquals(-1.00, $result);
}
public function testFormatValueReturnsFormattedDurationForNegativeStringValue(): void
{
$formatter = new DurationDecimalFormatter();
$result = $formatter->formatValue('-3600');
self::assertEquals(-1.00, $result);
}
}

View File

@@ -21,34 +21,34 @@ class DurationFormatterTest extends TestCase
{ {
$formatter = new DurationFormatter(); $formatter = new DurationFormatter();
$result = $formatter->formatValue(7200); $result = $formatter->formatValue(7200);
self::assertEquals(2.00, $result); self::assertEquals('2:00', $result);
} }
public function testFormatValueReturnsZeroForNonNumericValue(): void public function testFormatValueReturnsZeroForNonNumericValue(): void
{ {
$formatter = new DurationFormatter(); $formatter = new DurationFormatter();
$result = $formatter->formatValue('not a number'); $result = $formatter->formatValue('not a number');
self::assertEquals(0.0, $result); self::assertEquals('0:00', $result);
} }
public function testFormatValueReturnsFormattedDurationForFloatValue(): void public function testFormatValueReturnsFormattedDurationForFloatValue(): void
{ {
$formatter = new DurationFormatter(); $formatter = new DurationFormatter();
$result = $formatter->formatValue(4500.5); $result = $formatter->formatValue(4500.5);
self::assertEquals(1.25, $result); self::assertEquals('1:15', $result);
} }
public function testFormatValueReturnsZeroForNullValue(): void public function testFormatValueReturnsZeroForNullValue(): void
{ {
$formatter = new DurationFormatter(); $formatter = new DurationFormatter();
$result = $formatter->formatValue(null); $result = $formatter->formatValue(null);
self::assertEquals(0.0, $result); self::assertEquals('0:00', $result);
} }
public function testFormatValueReturnsFormattedDurationForNegativeValue(): void public function testFormatValueReturnsFormattedDurationForNegativeValue(): void
{ {
$formatter = new DurationFormatter(); $formatter = new DurationFormatter();
$result = $formatter->formatValue(-3600); $result = $formatter->formatValue(-3600);
self::assertEquals(-1.00, $result); self::assertEquals('-1:00', $result);
} }
} }

View File

@@ -459,6 +459,7 @@ class LocaleFormatExtensionsTest extends TestCase
'name' => null, 'name' => null,
'admin' => false, 'admin' => false,
'superAdmin' => false, 'superAdmin' => false,
'roles' => [],
], ],
]; ];
$user = $this->createMock(User::class); $user = $this->createMock(User::class);
@@ -488,6 +489,7 @@ class LocaleFormatExtensionsTest extends TestCase
'name' => 'anonymous', 'name' => 'anonymous',
'admin' => false, 'admin' => false,
'superAdmin' => false, 'superAdmin' => false,
'roles' => [],
], ],
]; ];