added API for handling work contract times (#4016)
This commit is contained in:
26
composer.lock
generated
26
composer.lock
generated
@@ -1449,16 +1449,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/orm",
|
||||
"version": "2.15.0",
|
||||
"version": "2.15.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/orm.git",
|
||||
"reference": "7b5fafffbeb560e75adc349e71bf1e90463796d2"
|
||||
"reference": "9bc6f5b4ac6f1e7d4248b2efbd01a748782075bc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/orm/zipball/7b5fafffbeb560e75adc349e71bf1e90463796d2",
|
||||
"reference": "7b5fafffbeb560e75adc349e71bf1e90463796d2",
|
||||
"url": "https://api.github.com/repos/doctrine/orm/zipball/9bc6f5b4ac6f1e7d4248b2efbd01a748782075bc",
|
||||
"reference": "9bc6f5b4ac6f1e7d4248b2efbd01a748782075bc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1494,7 +1494,7 @@
|
||||
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
|
||||
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
|
||||
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
|
||||
"vimeo/psalm": "4.30.0 || 5.9.0"
|
||||
"vimeo/psalm": "4.30.0 || 5.11.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "Provides support for XSD validation for XML mapping files",
|
||||
@@ -1544,9 +1544,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/orm/issues",
|
||||
"source": "https://github.com/doctrine/orm/tree/2.15.0"
|
||||
"source": "https://github.com/doctrine/orm/tree/2.15.1"
|
||||
},
|
||||
"time": "2023-04-26T18:52:02+00:00"
|
||||
"time": "2023-05-07T18:56:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/persistence",
|
||||
@@ -12153,16 +12153,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/diff",
|
||||
"version": "4.0.4",
|
||||
"version": "4.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/diff.git",
|
||||
"reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
|
||||
"reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
|
||||
"reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
|
||||
"reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12207,7 +12207,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/diff/issues",
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/4.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -12215,7 +12215,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2020-10-26T13:10:38+00:00"
|
||||
"time": "2023-05-07T05:35:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
|
||||
@@ -75,7 +75,7 @@ kimai:
|
||||
# PERMISSIONS
|
||||
# --------------------------------------------------------------------------------
|
||||
permissions:
|
||||
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and @SETS"])
|
||||
# mapping a "set name" to an array of "permission names"
|
||||
sets:
|
||||
ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','time_activity','delete_activity','permissions_activity']
|
||||
ACTIVITIES_ALL_TEAMLEAD: ['view_teamlead_activity','edit_teamlead_activity','budget_teamlead_activity','time_teamlead_activity','permissions_teamlead_activity']
|
||||
@@ -107,7 +107,7 @@ kimai:
|
||||
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
||||
# permissions which are deactivated, as these features are hidden for now
|
||||
# brave users can try to activate them and be surprised what happens
|
||||
REGISTER_BETA: []
|
||||
REGISTER_BETA: ['contract_other_profile']
|
||||
# mapping a "role name" to an array of "set names"
|
||||
maps:
|
||||
ROLE_USER: ['TIMESHEET','PROFILE']
|
||||
@@ -118,7 +118,9 @@ kimai:
|
||||
roles:
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
# TODO contract_other_profile
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data']
|
||||
# TODO contract_other_profile
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@@ -162,3 +164,7 @@ kimai:
|
||||
when@test:
|
||||
kimai:
|
||||
data_dir: '%kernel.project_dir%/tests/_data'
|
||||
permissions:
|
||||
roles:
|
||||
ROLE_ADMIN: ['contract_other_profile']
|
||||
ROLE_SUPER_ADMIN: ['contract_other_profile']
|
||||
|
||||
@@ -55,6 +55,7 @@ tabler:
|
||||
columns: fas fa-table
|
||||
comment: far fa-comment
|
||||
configuration: fas fa-cog
|
||||
contract: fas fa-balance-scale
|
||||
copy: far fa-copy
|
||||
create: fas fa-plus
|
||||
csv: fas fa-table
|
||||
@@ -109,6 +110,7 @@ tabler:
|
||||
repeat: fas fa-redo-alt
|
||||
reporting: far fa-chart-bar
|
||||
report: far fa-chart-bar
|
||||
review: fas fa-user-check
|
||||
right: fas fa-chevron-right
|
||||
right2: fas fa-angle-double-right
|
||||
roles: fas fa-user-shield
|
||||
@@ -136,4 +138,5 @@ tabler:
|
||||
weekly-times: fas fa-th
|
||||
widget_add: fas fa-folder-plus
|
||||
widget_remove: fas fa-folder-minus
|
||||
work_times: fas fa-calculator
|
||||
xlsx: fas fa-file-excel
|
||||
|
||||
@@ -224,3 +224,8 @@ services:
|
||||
class: App\Repository\BookmarkRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Bookmark']
|
||||
|
||||
App\Repository\WorkingTimeRepository:
|
||||
class: App\Repository\WorkingTimeRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\WorkingTime']
|
||||
|
||||
52
migrations/Version20230327143628.php
Normal file
52
migrations/Version20230327143628.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.1
|
||||
*/
|
||||
final class Version20230327143628 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Creates the working times table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$workingTimes = $schema->createTable('kimai2_working_times');
|
||||
|
||||
$workingTimes->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||
$workingTimes->addColumn('user_id', 'integer', ['notnull' => true]);
|
||||
$workingTimes->addColumn('approved_by', 'integer', ['notnull' => false, 'default' => null]);
|
||||
$workingTimes->addColumn('date', 'date', ['notnull' => true]);
|
||||
$workingTimes->addColumn('expected', 'integer', ['notnull' => true]);
|
||||
$workingTimes->addColumn('actual', 'integer', ['notnull' => true]);
|
||||
$workingTimes->addColumn('approved_at', 'datetime', ['notnull' => false, 'default' => null]);
|
||||
$workingTimes->setPrimaryKey(['id']);
|
||||
$workingTimes->addIndex(['user_id'], 'IDX_F95E4933A76ED395');
|
||||
$workingTimes->addIndex(['approved_by'], 'IDX_F95E49334EA3CB3D');
|
||||
$workingTimes->addUniqueIndex(['user_id', 'date'], 'UNIQ_F95E4933A76ED395AA9E377A');
|
||||
$workingTimes->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_F95E4933A76ED395');
|
||||
$workingTimes->addForeignKeyConstraint('kimai2_users', ['approved_by'], ['id'], ['onDelete' => 'SET NULL'], 'FK_F95E49334EA3CB3D');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$workingTimes = $schema->getTable('kimai2_working_times');
|
||||
|
||||
$workingTimes->removeForeignKey('FK_F95E4933A76ED395');
|
||||
$workingTimes->removeForeignKey('FK_F95E49334EA3CB3D');
|
||||
$schema->dropTable('kimai2_working_times');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
93
src/Controller/ContractController.php
Normal file
93
src/Controller/ContractController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkContractDetailControllerEvent;
|
||||
use App\Form\ContractByUserForm;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Utils\PageSetup;
|
||||
use App\WorkingTime\Model\BoxConfiguration;
|
||||
use App\WorkingTime\WorkingTimeService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Users can control their working time statistics
|
||||
*/
|
||||
final class ContractController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/contract', name: 'user_contract', methods: ['GET', 'POST'])]
|
||||
public function __invoke(Request $request, WorkingTimeService $workingTimeService, EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->isGranted('contract_other_profile');
|
||||
$defaultDate = $dateTimeFactory->createStartOfYear();
|
||||
|
||||
$values = new YearByUser();
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($defaultDate);
|
||||
|
||||
$form = $this->createFormForGetRequest(ContractByUserForm::class, $values, [
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($values->getUser() === null) {
|
||||
$values->setUser($currentUser);
|
||||
}
|
||||
|
||||
/** @var User $profile */
|
||||
$profile = $values->getUser();
|
||||
if ($this->getUser() !== $profile && !$canChangeUser) {
|
||||
throw $this->createAccessDeniedException('Cannot access user contract settings');
|
||||
}
|
||||
|
||||
if ($values->getDate() === null) {
|
||||
$values->setDate(clone $defaultDate);
|
||||
}
|
||||
|
||||
/** @var \DateTime $yearDate */
|
||||
$yearDate = $values->getDate();
|
||||
$year = $workingTimeService->getYear($profile, $yearDate);
|
||||
|
||||
$page = new PageSetup('status');
|
||||
$page->setHelp('contract.html');
|
||||
|
||||
// additional boxes by plugins
|
||||
$controllerEvent = new WorkContractDetailControllerEvent($year);
|
||||
$eventDispatcher->dispatch($controllerEvent);
|
||||
|
||||
$now = $dateTimeFactory->createDateTime();
|
||||
$summary = $workingTimeService->getYearSummary($year, $now);
|
||||
|
||||
$boxConfiguration = new BoxConfiguration();
|
||||
$boxConfiguration->setDecimal(false);
|
||||
$boxConfiguration->setCollapsed($profile->hasWorkHourConfiguration() && $summary->count() > 0);
|
||||
|
||||
return $this->render('contract/status.html.twig', [
|
||||
'box_configuration' => $boxConfiguration,
|
||||
'page_setup' => $page,
|
||||
'decimal' => $boxConfiguration->isDecimal(),
|
||||
'summaries' => $summary,
|
||||
'now' => $now,
|
||||
'boxes' => $controllerEvent->getController(),
|
||||
'year' => $year,
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\Model\TotpActivation;
|
||||
use App\Form\UserApiTokenType;
|
||||
use App\Form\UserContractType;
|
||||
use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\Form\UserPreferencesForm;
|
||||
@@ -178,6 +179,32 @@ final class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/contract', name: 'user_profile_contract', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('contract', 'profile')]
|
||||
public function contractAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createForm(UserContractType::class, $profile, [
|
||||
'action' => $this->generateUrl('user_profile_contract', ['username' => $profile->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$userRepository->saveUser($profile);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_contract', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/contract.html.twig', [
|
||||
'tab' => 'contract',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/teams', name: 'user_profile_teams', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('teams', 'profile')]
|
||||
public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response
|
||||
|
||||
@@ -1133,4 +1133,114 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
{
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
public function getWorkHoursMonday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_MONDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursTuesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursWednesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursThursday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursFriday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursSaturday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursSunday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, 0);
|
||||
}
|
||||
|
||||
public function getHolidaysPerYear(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursMonday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursTuesday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursWednesday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursThursday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursFriday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursSaturday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursSunday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setHolidaysPerYear(int $holidays): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, $holidays);
|
||||
}
|
||||
|
||||
public function hasContractSettings(): bool
|
||||
{
|
||||
return $this->hasWorkHourConfiguration() || $this->getHolidaysPerYear() !== 0;
|
||||
}
|
||||
|
||||
public function hasWorkHourConfiguration(): bool
|
||||
{
|
||||
return $this->getWorkHoursMonday() !== 0 ||
|
||||
$this->getWorkHoursTuesday() !== 0 ||
|
||||
$this->getWorkHoursWednesday() !== 0 ||
|
||||
$this->getWorkHoursThursday() !== 0 ||
|
||||
$this->getWorkHoursFriday() !== 0 ||
|
||||
$this->getWorkHoursSaturday() !== 0 ||
|
||||
$this->getWorkHoursSunday() !== 0;
|
||||
}
|
||||
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return match ($dateTime->format('N')) {
|
||||
'1' => $this->getWorkHoursMonday(),
|
||||
'2' => $this->getWorkHoursTuesday(),
|
||||
'3' => $this->getWorkHoursWednesday(),
|
||||
'4' => $this->getWorkHoursThursday(),
|
||||
'5' => $this->getWorkHoursFriday(),
|
||||
'6' => $this->getWorkHoursSaturday(),
|
||||
'7' => $this->getWorkHoursSunday(),
|
||||
default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d'))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ class UserPreference
|
||||
public const LOCALE = 'language';
|
||||
public const TIMEZONE = 'timezone';
|
||||
public const FIRST_WEEKDAY = 'first_weekday';
|
||||
public const WORK_HOURS_MONDAY = 'work_monday';
|
||||
public const WORK_HOURS_TUESDAY = 'work_tuesday';
|
||||
public const WORK_HOURS_WEDNESDAY = 'work_wednesday';
|
||||
public const WORK_HOURS_THURSDAY = 'work_thursday';
|
||||
public const WORK_HOURS_FRIDAY = 'work_friday';
|
||||
public const WORK_HOURS_SATURDAY = 'work_saturday';
|
||||
public const WORK_HOURS_SUNDAY = 'work_sunday';
|
||||
public const HOLIDAYS_PER_YEAR = 'holidays';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
||||
113
src/Entity/WorkingTime.php
Normal file
113
src/Entity/WorkingTime.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Repository\WorkingTimeRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Table(name: 'kimai2_working_times')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'date'])]
|
||||
#[ORM\Entity(repositoryClass: WorkingTimeRepository::class)]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class WorkingTime
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'date', type: 'date', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private \DateTimeInterface $date;
|
||||
#[ORM\Column(name: 'expected', type: 'integer', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private int $expectedTime = 0;
|
||||
#[ORM\Column(name: 'actual', type: 'integer', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private int $actualTime = 0;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?User $approvedBy = null;
|
||||
#[ORM\Column(name: 'approved_at', type: 'datetime', nullable: true)]
|
||||
#[Assert\NotNull]
|
||||
private ?\DateTimeInterface $approvedAt = null;
|
||||
|
||||
public function __construct(User $user, \DateTimeInterface $date)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getDate(): \DateTimeInterface
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
return $this->expectedTime;
|
||||
}
|
||||
|
||||
public function setExpectedTime(int $expectedTime): void
|
||||
{
|
||||
$this->expectedTime = $expectedTime;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
return $this->actualTime;
|
||||
}
|
||||
|
||||
public function setActualTime(int $actualTime): void
|
||||
{
|
||||
$this->actualTime = $actualTime;
|
||||
}
|
||||
|
||||
public function getApprovedBy(): ?User
|
||||
{
|
||||
return $this->approvedBy;
|
||||
}
|
||||
|
||||
public function setApprovedBy(?User $approvedBy): void
|
||||
{
|
||||
$this->approvedBy = $approvedBy;
|
||||
}
|
||||
|
||||
public function getApprovedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->approvedAt;
|
||||
}
|
||||
|
||||
public function setApprovedAt(?\DateTimeInterface $approvedAt): void
|
||||
{
|
||||
$this->approvedAt = $approvedAt;
|
||||
}
|
||||
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->approvedAt !== null;
|
||||
}
|
||||
}
|
||||
48
src/Event/WorkContractDetailControllerEvent.php
Normal file
48
src/Event/WorkContractDetailControllerEvent.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\WorkingTime\Model\Year;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Triggered for work-contract status pages, to add additional content boxes.
|
||||
*
|
||||
* @see https://symfony.com/doc/current/templates.html#embedding-controllers
|
||||
*/
|
||||
final class WorkContractDetailControllerEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $controller = [];
|
||||
|
||||
public function __construct(private Year $year)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function addController(string $controller): void
|
||||
{
|
||||
$this->controller[] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getController(): array
|
||||
{
|
||||
return $this->controller;
|
||||
}
|
||||
}
|
||||
37
src/Event/WorkingTimeYearSummaryEvent.php
Normal file
37
src/Event/WorkingTimeYearSummaryEvent.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use App\WorkingTime\Model\YearSummary;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeYearSummaryEvent extends Event
|
||||
{
|
||||
public function __construct(private YearPerUserSummary $yearPerUserSummary, private \DateTimeInterface $until)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->yearPerUserSummary->getYear();
|
||||
}
|
||||
|
||||
public function getUntil(): \DateTimeInterface
|
||||
{
|
||||
return $this->until;
|
||||
}
|
||||
|
||||
public function addSummary(YearSummary $yearSummary): void
|
||||
{
|
||||
$this->yearPerUserSummary->addSummary($yearSummary);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $user)) {
|
||||
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'edit', 'translation_domain' => 'actions']);
|
||||
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats', 'translation_domain' => 'actions']);
|
||||
}
|
||||
if ($this->isGranted('password', $user)) {
|
||||
$event->addAction('password', ['url' => $this->path('user_profile_password', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.password']);
|
||||
@@ -48,5 +48,8 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
|
||||
if ($this->isGranted('roles', $user)) {
|
||||
$event->addAction('roles', ['url' => $this->path('user_profile_roles', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.roles']);
|
||||
}
|
||||
if ($this->isGranted('contract', $user)) {
|
||||
$event->addAction('contract', ['url' => $this->path('user_profile_contract', ['username' => $user->getUserIdentifier()]), 'title' => 'work_contract']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use App\Utils\MenuItemModel;
|
||||
use KevinPapst\TablerBundle\Helper\ContextHelper;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
final class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private AuthorizationCheckerInterface $security, private ContextHelper $helper)
|
||||
public function __construct(private Security $security, private ContextHelper $helper)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -45,6 +46,8 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
// main menu
|
||||
$menu = $event->getMenu();
|
||||
/** @var User $user */
|
||||
$user = $auth->getUser();
|
||||
|
||||
$menu->addChild(new MenuItemModel('dashboard', 'dashboard.title', 'dashboard', [], 'dashboard'));
|
||||
$menu->addChild(new MenuItemModel('favorites', 'favorite_routes', null, [], 'bookmarked'));
|
||||
@@ -87,6 +90,15 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu->addChild($times);
|
||||
}
|
||||
|
||||
$contract = new MenuItemModel('contract', 'work_contract', null, [], 'contract');
|
||||
if ($user->hasContractSettings() || $auth->isGranted('contract_other_profile')) {
|
||||
$contract->addChild(new MenuItemModel('contract_status', 'work_times', 'user_contract', [], 'work_times'));
|
||||
}
|
||||
|
||||
if ($contract->hasChildren()) {
|
||||
$menu->addChild($contract);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_reporting')) {
|
||||
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], 'reporting');
|
||||
$reporting->setChildRoutes(['report_user_week', 'report_user_month', 'report_weekly_users', 'report_monthly_users', 'report_project_view']);
|
||||
|
||||
48
src/Form/ContractByUserForm.php
Normal file
48
src/Form/ContractByUserForm.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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;
|
||||
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YearPickerType;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ContractByUserForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['width' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearByUser::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'include_user' => false,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
63
src/Form/UserContractType.php
Normal file
63
src/Form/UserContractType.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DurationType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<User>
|
||||
*/
|
||||
final class UserContractType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$dayOptions = [
|
||||
'translation_domain' => 'system-configuration',
|
||||
'constraints' => [
|
||||
new GreaterThanOrEqual(0)
|
||||
],
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('workHoursMonday', DurationType::class, array_merge(['label' => 'Monday'], $dayOptions))
|
||||
->add('workHoursTuesday', DurationType::class, array_merge(['label' => 'Tuesday'], $dayOptions))
|
||||
->add('workHoursWednesday', DurationType::class, array_merge(['label' => 'Wednesday'], $dayOptions))
|
||||
->add('workHoursThursday', DurationType::class, array_merge(['label' => 'Thursday'], $dayOptions))
|
||||
->add('workHoursFriday', DurationType::class, array_merge(['label' => 'Friday'], $dayOptions))
|
||||
->add('workHoursSaturday', DurationType::class, array_merge(['label' => 'Saturday'], $dayOptions))
|
||||
->add('workHoursSunday', DurationType::class, array_merge(['label' => 'Sunday'], $dayOptions))
|
||||
/*
|
||||
->add('holidaysPerYear', IntegerType::class, [
|
||||
'label' => false,
|
||||
'constraints' => [
|
||||
new Range(['min' => 0, 'max' => 365])
|
||||
],
|
||||
])
|
||||
*/
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_user_contract',
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
src/Model/Day.php
Normal file
24
src/Model/Day.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Day
|
||||
{
|
||||
public function __construct(private DateTimeInterface $day)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDay(): DateTimeInterface
|
||||
{
|
||||
return $this->day;
|
||||
}
|
||||
}
|
||||
54
src/Model/Month.php
Normal file
54
src/Model/Month.php
Normal 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;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Month
|
||||
{
|
||||
/**
|
||||
* @var Day[]
|
||||
*/
|
||||
private array $days = [];
|
||||
|
||||
public function __construct(private \DateTimeInterface $month)
|
||||
{
|
||||
$date = new \DateTimeImmutable($this->month->format('Y-m-01 00:00:00'));
|
||||
$start = $date->format('m');
|
||||
while ($start === $date->format('m')) {
|
||||
$day = $this->createDay($date);
|
||||
$this->setDay($day);
|
||||
$date = $date->add(new \DateInterval('P1D'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDay(\DateTimeInterface $day): Day
|
||||
{
|
||||
return new Day($day);
|
||||
}
|
||||
|
||||
public function getMonth(): DateTimeInterface
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
protected function setDay(Day $day): void
|
||||
{
|
||||
$this->days['_' . $day->getDay()->format('d')] = $day;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Day[]
|
||||
*/
|
||||
public function getDays(): array
|
||||
{
|
||||
return array_values($this->days);
|
||||
}
|
||||
}
|
||||
60
src/Model/Year.php
Normal file
60
src/Model/Year.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Year
|
||||
{
|
||||
/**
|
||||
* @var Month[]
|
||||
*/
|
||||
private array $months = [];
|
||||
|
||||
public function __construct(private DateTimeInterface $month)
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable();
|
||||
$monthDate = $monthDate->setDate((int) $this->month->format('Y'), 1, 1);
|
||||
$monthDate = $monthDate->setTime(1, 0);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$month = $this->createMonth($monthDate);
|
||||
$this->setMonth($month);
|
||||
$monthDate = $monthDate->add(new \DateInterval('P1M'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return new Month($month);
|
||||
}
|
||||
|
||||
public function getYear(): DateTimeInterface
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
protected function setMonth(Month $month): void
|
||||
{
|
||||
$this->months['_' . $month->getMonth()->format('m')] = $month;
|
||||
}
|
||||
|
||||
public function getMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return $this->months['_' . $month->format('m')];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Month[]
|
||||
*/
|
||||
public function getMonths(): array
|
||||
{
|
||||
return array_values($this->months);
|
||||
}
|
||||
}
|
||||
89
src/Repository/WorkingTimeRepository.php
Normal file
89
src/Repository/WorkingTimeRepository.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
/**
|
||||
* @extends EntityRepository<WorkingTime>
|
||||
* @internal use WorkingTimeService instead!
|
||||
*/
|
||||
class WorkingTimeRepository extends EntityRepository
|
||||
{
|
||||
public function deleteWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($workingTime);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function saveWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($workingTime);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void
|
||||
{
|
||||
$this->getEntityManager()->persist($workingTime);
|
||||
}
|
||||
|
||||
public function persistScheduledWorkingTimes(): void
|
||||
{
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<WorkingTime>
|
||||
*/
|
||||
public function findForYear(User $user, \DateTimeInterface $year): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->select('w')
|
||||
->where($qb->expr()->eq('w.user', ':user'))
|
||||
->setParameter('user', $user->getId())
|
||||
->andWhere($qb->expr()->eq('YEAR(w.date)', ':date'))
|
||||
->setParameter('date', $year->format('Y'))
|
||||
->indexBy('w', 'w.date')
|
||||
->orderBy('w.date')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getLatestApproval(User $user): ?WorkingTime
|
||||
{
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->select('MAX(DATE(w.date))')
|
||||
->where($qb->expr()->eq('w.user', ':user'))
|
||||
->setParameter('user', $user->getId())
|
||||
->andWhere($qb->expr()->isNotNull('w.approvedAt'))
|
||||
;
|
||||
|
||||
$date = $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
if ($date === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->select('w')
|
||||
->where($qb->expr()->eq('w.user', ':user'))
|
||||
->setParameter('user', $user->getId())
|
||||
->andWhere($qb->expr()->eq('DATE(w.date)', 'DATE(:date)'))
|
||||
->setParameter('date', $date)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult(); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ final class UserVoter extends Voter
|
||||
'api-token',
|
||||
'hourly-rate',
|
||||
'view_team_member',
|
||||
'contract',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
@@ -60,6 +61,10 @@ final class UserVoter extends Voter
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($attribute === 'contract') {
|
||||
return $this->permissionManager->hasRolePermission($user, 'contract_other_profile');
|
||||
}
|
||||
|
||||
if ($attribute === 'view_team_member') {
|
||||
if ($subject->getId() !== $user->getId()) {
|
||||
return false;
|
||||
|
||||
36
src/WorkingTime/Model/BoxConfiguration.php
Normal file
36
src/WorkingTime/Model/BoxConfiguration.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
final class BoxConfiguration
|
||||
{
|
||||
private bool $decimal = false;
|
||||
private bool $collapsed = false;
|
||||
|
||||
public function setDecimal(bool $decimal): void
|
||||
{
|
||||
$this->decimal = $decimal;
|
||||
}
|
||||
|
||||
public function setCollapsed(bool $collapsed): void
|
||||
{
|
||||
$this->collapsed = $collapsed;
|
||||
}
|
||||
|
||||
public function isDecimal(): bool
|
||||
{
|
||||
return $this->decimal;
|
||||
}
|
||||
|
||||
public function isCollapsed(): bool
|
||||
{
|
||||
return $this->collapsed;
|
||||
}
|
||||
}
|
||||
28
src/WorkingTime/Model/Day.php
Normal file
28
src/WorkingTime/Model/Day.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Model\Day as BaseDay;
|
||||
|
||||
final class Day extends BaseDay
|
||||
{
|
||||
private ?WorkingTime $workingTime = null;
|
||||
|
||||
public function getWorkingTime(): ?WorkingTime
|
||||
{
|
||||
return $this->workingTime;
|
||||
}
|
||||
|
||||
public function setWorkingTime(?WorkingTime $workingTime): void
|
||||
{
|
||||
$this->workingTime = $workingTime;
|
||||
}
|
||||
}
|
||||
97
src/WorkingTime/Model/Month.php
Normal file
97
src/WorkingTime/Model/Month.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Month as BaseMonth;
|
||||
|
||||
/**
|
||||
* @method array<Day> getDays()
|
||||
*/
|
||||
final class Month extends BaseMonth
|
||||
{
|
||||
private ?bool $locked = null;
|
||||
|
||||
/**
|
||||
* A month is only locked IF every day is approved.
|
||||
* If there is even one day left open, the entire month is not locked.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocked(): bool
|
||||
{
|
||||
if ($this->locked === null) {
|
||||
$this->locked = true;
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null && !$day->getWorkingTime()->isApproved()) {
|
||||
$this->locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
public function getLockDate(): ?\DateTimeInterface
|
||||
{
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null && $day->getWorkingTime()->isApproved()) {
|
||||
return $day->getWorkingTime()->getApprovedAt();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLockedBy(): ?User
|
||||
{
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null && $day->getWorkingTime()->isApproved()) {
|
||||
return $day->getWorkingTime()->getApprovedBy();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function createDay(\DateTimeInterface $day): Day
|
||||
{
|
||||
return new Day($day);
|
||||
}
|
||||
|
||||
public function getExpectedTime(\DateTimeInterface $until): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($until < $day->getDay()) {
|
||||
break;
|
||||
}
|
||||
if ($day->getWorkingTime() !== null) {
|
||||
$time += $day->getWorkingTime()->getExpectedTime();
|
||||
}
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null) {
|
||||
$time += $day->getWorkingTime()->getActualTime();
|
||||
}
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
38
src/WorkingTime/Model/MonthSummary.php
Normal file
38
src/WorkingTime/Model/MonthSummary.php
Normal 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\WorkingTime\Model;
|
||||
|
||||
use App\Model\Month as BaseMonth;
|
||||
|
||||
final class MonthSummary extends BaseMonth
|
||||
{
|
||||
private int $expectedTime = 0;
|
||||
private int $actualTime = 0;
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
return $this->expectedTime;
|
||||
}
|
||||
|
||||
public function setExpectedTime(int $expectedTime): void
|
||||
{
|
||||
$this->expectedTime = $expectedTime;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
return $this->actualTime;
|
||||
}
|
||||
|
||||
public function setActualTime(int $actualTime): void
|
||||
{
|
||||
$this->actualTime = $actualTime;
|
||||
}
|
||||
}
|
||||
57
src/WorkingTime/Model/Year.php
Normal file
57
src/WorkingTime/Model/Year.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Year as BaseYear;
|
||||
|
||||
/**
|
||||
* @method array<Month> getMonths()
|
||||
* @method Month getMonth(\DateTimeInterface $month)
|
||||
*/
|
||||
final class Year extends BaseYear
|
||||
{
|
||||
public function __construct(\DateTimeInterface $month, private User $user)
|
||||
{
|
||||
parent::__construct($month);
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return new Month($month);
|
||||
}
|
||||
|
||||
public function getExpectedTime(\DateTimeInterface $until): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$time += $month->getExpectedTime($until);
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$time += $month->getActualTime();
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
78
src/WorkingTime/Model/YearPerUserSummary.php
Normal file
78
src/WorkingTime/Model/YearPerUserSummary.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* @implements \IteratorAggregate<int, YearSummary>
|
||||
*/
|
||||
final class YearPerUserSummary implements \Countable, \IteratorAggregate
|
||||
{
|
||||
/** @var array<YearSummary> */
|
||||
private array $summaries = [];
|
||||
|
||||
public function __construct(private Year $year)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->year->getUser();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->summaries);
|
||||
}
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
return new \ArrayIterator($this->summaries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return YearSummary[]
|
||||
*/
|
||||
public function getSummaries(): array
|
||||
{
|
||||
return $this->summaries;
|
||||
}
|
||||
|
||||
public function addSummary(YearSummary $summary): void
|
||||
{
|
||||
$this->summaries[] = $summary;
|
||||
}
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getSummaries() as $month) {
|
||||
$all += $month->getExpectedTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getSummaries() as $month) {
|
||||
$all += $month->getActualTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
54
src/WorkingTime/Model/YearSummary.php
Normal file
54
src/WorkingTime/Model/YearSummary.php
Normal 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\WorkingTime\Model;
|
||||
|
||||
use App\Model\Year as BaseYear;
|
||||
|
||||
/**
|
||||
* @method array<MonthSummary> getMonths()
|
||||
* @method MonthSummary getMonth(\DateTimeInterface $month)
|
||||
*/
|
||||
final class YearSummary extends BaseYear
|
||||
{
|
||||
public function __construct(\DateTimeInterface $month, private string $title)
|
||||
{
|
||||
parent::__construct($month);
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeInterface $month): MonthSummary
|
||||
{
|
||||
return new MonthSummary($month);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$all += $month->getExpectedTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$all += $month->getActualTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
156
src/WorkingTime/WorkingTimeService.php
Normal file
156
src/WorkingTime/WorkingTimeService.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?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\WorkingTime;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\WorkingTimeRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal this API and the entire namespace is instable: you should expect changes!
|
||||
*/
|
||||
final class WorkingTimeService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $timesheetRepository, private WorkingTimeRepository $workingTimeRepository, private EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYearSummary(Year $year, \DateTimeInterface $until): YearPerUserSummary
|
||||
{
|
||||
$yearPerUserSummary = new YearPerUserSummary($year);
|
||||
|
||||
$summaryEvent = new WorkingTimeYearSummaryEvent($yearPerUserSummary, $until);
|
||||
$this->eventDispatcher->dispatch($summaryEvent);
|
||||
|
||||
return $yearPerUserSummary;
|
||||
}
|
||||
|
||||
public function getLatestApproval(User $user): ?WorkingTime
|
||||
{
|
||||
return $this->workingTimeRepository->getLatestApproval($user);
|
||||
}
|
||||
|
||||
public function getYear(User $user, \DateTimeInterface $yearDate): Year
|
||||
{
|
||||
$yearTimes = $this->workingTimeRepository->findForYear($user, $yearDate);
|
||||
$existing = [];
|
||||
foreach ($yearTimes as $workingTime) {
|
||||
$existing[$workingTime->getDate()->format('Y-m-d')] = $workingTime;
|
||||
}
|
||||
|
||||
$year = new Year(\DateTimeImmutable::createFromInterface($yearDate), $user);
|
||||
|
||||
$stats = null;
|
||||
|
||||
foreach ($year->getMonths() as $month) {
|
||||
foreach ($month->getDays() as $day) {
|
||||
$key = $day->getDay()->format('Y-m-d');
|
||||
if (\array_key_exists($key, $existing)) {
|
||||
$day->setWorkingTime($existing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($stats === null) {
|
||||
$stats = $this->getYearStatistics($yearDate, $user);
|
||||
}
|
||||
|
||||
$result = new WorkingTime($user, $day->getDay());
|
||||
$result->setExpectedTime($user->getWorkHoursForDay($day->getDay()));
|
||||
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
$result->setActualTime($stats[$key]);
|
||||
}
|
||||
|
||||
$day->setWorkingTime($result);
|
||||
}
|
||||
}
|
||||
|
||||
return $year;
|
||||
}
|
||||
|
||||
public function getMonth(User $user, \DateTimeInterface $monthDate): Month
|
||||
{
|
||||
// TODO improve me, do not calculate the entire year for that
|
||||
$year = $this->getYear($user, $monthDate);
|
||||
|
||||
return $year->getMonth($monthDate);
|
||||
}
|
||||
|
||||
public function approveMonth(Month $month, \DateTimeInterface $approvalDate, User $approver): void
|
||||
{
|
||||
$update = false;
|
||||
|
||||
foreach ($month->getDays() as $day) {
|
||||
$workingTime = $day->getWorkingTime();
|
||||
if ($workingTime === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($workingTime->getId() !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($month->isLocked() || $workingTime->isApproved()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$workingTime->setApprovedBy($approver);
|
||||
$workingTime->setApprovedAt($approvalDate);
|
||||
$this->workingTimeRepository->scheduleWorkingTimeUpdate($workingTime);
|
||||
$update = true;
|
||||
}
|
||||
|
||||
if ($update) {
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $year
|
||||
* @param User $user
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function getYearStatistics(\DateTimeInterface $year, User $user): array
|
||||
{
|
||||
$dateTimeFactory = DateTimeFactory::createByUser($user);
|
||||
$begin = $dateTimeFactory->createStartOfYear($year);
|
||||
$end = $dateTimeFactory->createEndOfYear($year);
|
||||
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
|
||||
$qb
|
||||
->select('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('DATE(t.date) as day')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('user', $user->getId())
|
||||
->addGroupBy('day')
|
||||
;
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$durations = [];
|
||||
foreach ($results as $row) {
|
||||
$durations[$row['day']] = (int) $row['duration'];
|
||||
}
|
||||
|
||||
return $durations; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
311
templates/contract/status.html.twig
Normal file
311
templates/contract/status.html.twig
Normal file
@@ -0,0 +1,311 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set withWorkHourConfiguration = user.hasWorkHourConfiguration() %}
|
||||
|
||||
{% if form is defined %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
{{ form_start(form, {'attr': {'class': 'form-reporting', 'id': 'report-form'}}) }}
|
||||
<div class="btn-list w-100">
|
||||
{{ form_widget(form.date) }}
|
||||
{% if form.user is defined %}
|
||||
{{ form_widget(form.user, {'label': false}) }}
|
||||
{% elseif app.user != user %}
|
||||
{{ widgets.username(user) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not withWorkHourConfiguration %}
|
||||
{{ widgets.alert('info', 'work_times_should.none_configured'|trans) }}
|
||||
{% endif %}
|
||||
|
||||
{% if summaries|length > 0 %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {'fullsize': true} %}
|
||||
{% from "macros/status.html.twig" import status_duration %}
|
||||
{% from "macros/widgets.html.twig" import work_times_result %}
|
||||
{% block box_title %}
|
||||
{{ 'export.summary'|trans }}
|
||||
{{ status_duration(work_times_result(summaries.expectedTime, summaries.actualTime, decimal)) }}
|
||||
{% endblock %}
|
||||
{% block box_body_classes %}card-table table-responsive{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-vcenter table-hover dataTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-end total">{{ 'sum.total'|trans }}</th>
|
||||
{% for month in year.months %}
|
||||
<th class="text-end">{{ month.month|month_name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set monthTotalShould = {'_01': 0, '_02': 0, '_03': 0, '_04': 0, '_05': 0, '_06': 0, '_07': 0, '_08': 0, '_09': 0, '_10': 0, '_11': 0, '_12': 0} %}
|
||||
{% set monthTotalIs = {'_01': 0, '_02': 0, '_03': 0, '_04': 0, '_05': 0, '_06': 0, '_07': 0, '_08': 0, '_09': 0, '_10': 0, '_11': 0, '_12': 0} %}
|
||||
{% for summary in summaries %}
|
||||
<tr>
|
||||
<td>{{ summary.title|trans }}</td>
|
||||
<td class="text-end total">
|
||||
{% if summary.expectedTime > 0 %}
|
||||
{{ work_times_result(summary.expectedTime, summary.actualTime, decimal) }}
|
||||
{% else %}
|
||||
{{ summary.actualTime|duration(decimal) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% for month in summary.months %}
|
||||
{% set monthShould = month.expectedTime %}
|
||||
{% set monthIs = month.actualTime %}
|
||||
{% set dateKey = '_' ~ month.month.format('m') %}
|
||||
{% set monthTotalShouldTmp = monthTotalShould[dateKey] + monthShould %}
|
||||
{% set monthTotalIsTmp = monthTotalIs[dateKey] + monthIs %}
|
||||
{% set monthTotalShould = monthTotalShould|merge({(dateKey): monthTotalShouldTmp}) %}
|
||||
{% set monthTotalIs = monthTotalIs|merge({(dateKey): monthTotalIsTmp}) %}
|
||||
<td class="text-end">
|
||||
{% if now < month.month and monthShould > 0 %}
|
||||
{{ monthShould|duration(decimal) }}
|
||||
{% else %}
|
||||
{{ work_times_result(monthShould, monthIs, decimal) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="summary">
|
||||
<td></td>
|
||||
<td class="text-end total">
|
||||
{{ work_times_result(summaries.expectedTime, summaries.actualTime, decimal) }}
|
||||
</td>
|
||||
{% for month in year.months %}
|
||||
{% set dateKey = '_' ~ month.month.format('m') %}
|
||||
<td class="text-end">
|
||||
{{ work_times_result(monthTotalShould[dateKey], monthTotalIs[dateKey], decimal) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
{% if withWorkHourConfiguration %}
|
||||
{% set expectedWeekTimes = user.workHoursMonday + user.workHoursTuesday + user.workHoursWednesday + user.workHoursThursday + user.workHoursFriday + user.workHoursSaturday + user.workHoursSunday %}
|
||||
|
||||
{% embed '@theme/embeds/card.html.twig' with {'collapsed': box_configuration.collapsed} %}
|
||||
{% from "macros/status.html.twig" import status_duration %}
|
||||
{% block box_title %}
|
||||
{{ 'work_times_should'|trans }}
|
||||
{{ status_duration(expectedWeekTimes|duration) }}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'agendaWeek'|trans }}</div>
|
||||
<div class="datagrid-content">{{ expectedWeekTimes|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Monday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursMonday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Tuesday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursTuesday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Wednesday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursWednesday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Thursday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursThursday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Friday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursFriday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Saturday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursSaturday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Sunday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursSunday|duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
{% if year is defined %}
|
||||
{% embed '@theme/embeds/card.html.twig' with {'collapsed': box_configuration.collapsed, 'id': 'working_time_detail_box', 'fullsize': true} %}
|
||||
{% set yearShould = 0 %}
|
||||
{% set yearIs = 0 %}
|
||||
{% set approved = 0 %}
|
||||
{% for month in year.months %}
|
||||
{% set monthShould = month.expectedTime(now) %}
|
||||
{% set monthIs = month.actualTime %}
|
||||
{% set showStats = now > month.month or monthIs > 0 %}
|
||||
{% if showStats %}
|
||||
{% set yearShould = yearShould + monthShould %}
|
||||
{% endif %}
|
||||
{% set yearIs = yearIs + monthIs %}
|
||||
{% if month.locked %}
|
||||
{% set approved = approved + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% from "macros/widgets.html.twig" import work_times_result, table_actions, card_tool_button, username %}
|
||||
{% from "macros/status.html.twig" import status_count %}
|
||||
{% block box_title %}
|
||||
{{ 'work_times'|trans }}
|
||||
{% if withWorkHourConfiguration %}
|
||||
{{ status_count(approved ~ '/' ~ year.months|length) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_body_classes %}card-table table-responsive{% endblock %}
|
||||
{% block box_tools %}
|
||||
{# {{ card_tool_button('visibility', {'onclick': "toggleDays(); return false;", 'id': 'toggleDaysIcon'}) }} #}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<table id="working_times_details" class="table table-vcenter table-hover dataTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if withWorkHourConfiguration %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
<th>{{ 'month'|trans }}</th>
|
||||
<th class="text-end total">{{ 'sum.total'|trans }}</th>
|
||||
<th class="text-end">{{ 'work_times_should'|trans }}</th>
|
||||
<th class="text-end">{{ 'work_times_is'|trans }}</th>
|
||||
{% for day in 1..31 %}
|
||||
<th class="text-end contractDay">{% if day < 10 %}0{% endif %}{{ day }}</th>
|
||||
{% endfor %}
|
||||
<th class="actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for month in year.months %}
|
||||
{% set monthShould = month.expectedTime(now) %}
|
||||
{% set monthIs = month.actualTime %}
|
||||
{% set showStats = now > month.month or monthIs > 0 %}
|
||||
<tr>
|
||||
{% if withWorkHourConfiguration %}
|
||||
<td class="w-min">
|
||||
{% if month.locked %}
|
||||
<i class="{{ 'locked'|icon }}" data-toggle="tooltip" title="{% if month.lockedBy is not null %}{{ month.lockedBy.displayName }}{% else %}{{ 'deleted'|trans }}{% endif %}: {{ month.lockDate|date_short }}"></i>
|
||||
{% else %}
|
||||
<i class="bg-yellow-lt {{ 'unlocked'|icon }}"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-nowrap w-min">
|
||||
{{ month.month|month_name }}
|
||||
</td>
|
||||
<td class="text-end total w-min">
|
||||
{% if showStats %}
|
||||
{{ work_times_result(monthShould, monthIs, decimal) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end w-min">
|
||||
{{ monthShould|duration(decimal) }}
|
||||
</td>
|
||||
<td class="text-end w-min">
|
||||
{{ monthIs|duration(decimal) }}
|
||||
</td>
|
||||
{% set dayCount = 0 %}
|
||||
{% for day in month.days %}
|
||||
{% set class = 'text-end contractDay' %}
|
||||
{% if day.day is weekend %}
|
||||
{% set class = class ~ ' weekend' %}
|
||||
{% endif %}
|
||||
{% if day.workingTime.expectedTime > 0 and day.workingTime.actualTime == 0 and now > day.day %}
|
||||
{% set class = class ~ ' bg-orange-lt' %}
|
||||
{% elseif day.workingTime.expectedTime == 0 and day.workingTime.actualTime > 0 and now > day.day %}
|
||||
{% set class = class ~ ' bg-yellow-lt' %}
|
||||
{% endif %}
|
||||
<td class="{{ class }}">
|
||||
{% if (day.workingTime.expectedTime != 0 or day.workingTime.actualTime != 0) and (now > day.day or day.workingTime.actualTime > 0) %}
|
||||
{{ work_times_result(day.workingTime.expectedTime, day.workingTime.actualTime, decimal) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% set dayCount = dayCount + 1 %}
|
||||
{% endfor %}
|
||||
{% if dayCount < 31 %}
|
||||
{% for day in 1..(31-dayCount)%}
|
||||
<td class="contractDay"></td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<td class="actions">
|
||||
{% set event = actions(app.user, 'contract_month', 'index', {'year': year, 'month': month}) %}
|
||||
{{ table_actions(event.actions) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="summary">
|
||||
{% if withWorkHourConfiguration %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td></td>
|
||||
<td class="text-end total">
|
||||
{{ work_times_result(yearShould, yearIs, decimal) }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{{ yearShould|duration(decimal) }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{{ yearIs|duration(decimal) }}
|
||||
</td>
|
||||
{% for day in 1..31 %}
|
||||
<td class="contractDay"></td>
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
{% for controller in boxes %}
|
||||
{{ render(controller(controller, {'year': year, 'boxConfiguration': box_configuration})) }}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
function toggleDays()
|
||||
{
|
||||
document.querySelectorAll('table#working_times_details .contractDay').forEach(
|
||||
element => {
|
||||
element.classList.toggle('d-none');
|
||||
}
|
||||
);
|
||||
const icon = document.querySelector('#toggleDaysIcon i');
|
||||
icon.classList.toggle('fa-eye');
|
||||
icon.classList.toggle('fa-eye-slash');
|
||||
}
|
||||
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.workingTimesUpdate', true);
|
||||
|
||||
const contractForm = document.getElementById('report-form');
|
||||
if (contractForm !== null) {
|
||||
contractForm.addEventListener('change', () => {
|
||||
contractForm.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -514,4 +514,17 @@
|
||||
KimaiReloadPageWidget.create('{{ events }}', {% if full_reload is same as true %}true{% else %}false{% endif %});
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro work_times_result(should, actual, decimal) %}
|
||||
{% set result = actual - should %}
|
||||
{% if result == 0 %}
|
||||
<span class="work_time_exact">{{ result|duration(decimal) }}</span>
|
||||
{% elseif should == 0 and actual > 0 %}
|
||||
<span class="work_time_positive text-green">+{{ actual|duration(decimal) }}</span>
|
||||
{% elseif result < 0 %}
|
||||
<span class="work_time_negative text-red">{{ result|duration(decimal) }}</span>
|
||||
{% else %}
|
||||
<span class="work_time_positive text-green">+{{ result|duration(decimal) }}</span>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
41
templates/user/contract.html.twig
Normal file
41
templates/user/contract.html.twig
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends 'user/form.html.twig' %}
|
||||
|
||||
{% block form_content %}
|
||||
|
||||
<h3>{{ 'work_times_should'|trans }}</h3>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Monday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Tuesday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Wednesday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Thursday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Friday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Saturday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Sunday'|trans({}, 'system-configuration') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ form_widget(form.workHoursMonday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursTuesday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursWednesday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursThursday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursFriday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursSaturday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursSunday) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>{{ 'work_times_should.help'|trans }}</p>
|
||||
|
||||
{% if form.holidaysPerYear is defined %}
|
||||
<h3>{{ 'pto_year_holiday'|trans }}</h3>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
{% block form_body %}
|
||||
{{ form_start(form) }}
|
||||
<div class="card-body">
|
||||
<div class="card-body {% block form_body_class %}{% endblock %}">
|
||||
{% block form_pre_content %}{% endblock %}
|
||||
{% block form_content %}
|
||||
{{ form_widget(form) }}
|
||||
|
||||
45
tests/Controller/ContractControllerTest.php
Normal file
45
tests/Controller/ContractControllerTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
*/
|
||||
class ContractControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure(): void
|
||||
{
|
||||
$this->assertUrlIsSecured('/contract');
|
||||
}
|
||||
|
||||
public function testIndexAction(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/contract');
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertNotFalse($content);
|
||||
self::assertStringContainsString('No target hours have been configured', $content);
|
||||
$node = $client->getCrawler()->filter('select#user');
|
||||
self::assertEquals(0, $node->count());
|
||||
}
|
||||
|
||||
public function testAdminCanChangeUser(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/contract');
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertNotFalse($content);
|
||||
self::assertStringContainsString('No target hours have been configured', $content);
|
||||
$node = $client->getCrawler()->filter('select#user');
|
||||
self::assertEquals(1, $node->count());
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class PermissionControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/permissions');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 129);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 130);
|
||||
$this->assertPageActions($client, [
|
||||
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||
]);
|
||||
|
||||
@@ -541,4 +541,56 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/2fa_deactivate', 'POST');
|
||||
}
|
||||
|
||||
public function testContractActionIsSecured(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/contract');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
}
|
||||
|
||||
public function testContractAction(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/contract');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$user = $this->getUserByRole(User::ROLE_USER);
|
||||
|
||||
$this->assertEquals(0, $user->getWorkHoursMonday());
|
||||
$this->assertEquals(0, $user->getWorkHoursTuesday());
|
||||
$this->assertEquals(0, $user->getWorkHoursWednesday());
|
||||
$this->assertEquals(0, $user->getWorkHoursThursday());
|
||||
$this->assertEquals(0, $user->getWorkHoursFriday());
|
||||
$this->assertEquals(0, $user->getWorkHoursSaturday());
|
||||
$this->assertEquals(0, $user->getWorkHoursSunday());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=user_contract]')->form();
|
||||
|
||||
$client->submit($form, [
|
||||
'user_contract[workHoursMonday]' => '1:00',
|
||||
'user_contract[workHoursTuesday]' => '2:00',
|
||||
'user_contract[workHoursWednesday]' => '3:00',
|
||||
'user_contract[workHoursThursday]' => '4:30',
|
||||
'user_contract[workHoursFriday]' => '5:12',
|
||||
'user_contract[workHoursSaturday]' => '6:59',
|
||||
'user_contract[workHoursSunday]' => '0:01',
|
||||
]);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode(UserFixtures::USERNAME_USER) . '/contract'));
|
||||
$client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$this->assertHasFlashSuccess($client);
|
||||
|
||||
$user = $this->getUserByRole(User::ROLE_USER);
|
||||
|
||||
$this->assertEquals(3600, $user->getWorkHoursMonday());
|
||||
$this->assertEquals(7200, $user->getWorkHoursTuesday());
|
||||
$this->assertEquals(10800, $user->getWorkHoursWednesday());
|
||||
$this->assertEquals(16200, $user->getWorkHoursThursday());
|
||||
$this->assertEquals(18720, $user->getWorkHoursFriday());
|
||||
$this->assertEquals(25140, $user->getWorkHoursSaturday());
|
||||
$this->assertEquals(60, $user->getWorkHoursSunday());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,52 @@ class UserTest extends TestCase
|
||||
|
||||
$user->setAccountNumber('A-058375');
|
||||
self::assertEquals('A-058375', $user->getAccountNumber());
|
||||
|
||||
self::assertEquals(0, $user->getWorkHoursMonday());
|
||||
self::assertEquals(0, $user->getWorkHoursTuesday());
|
||||
self::assertEquals(0, $user->getWorkHoursWednesday());
|
||||
self::assertEquals(0, $user->getWorkHoursThursday());
|
||||
self::assertEquals(0, $user->getWorkHoursFriday());
|
||||
self::assertEquals(0, $user->getWorkHoursSaturday());
|
||||
self::assertEquals(0, $user->getWorkHoursSunday());
|
||||
self::assertEquals(0, $user->getHolidaysPerYear());
|
||||
self::assertFalse($user->hasWorkHourConfiguration());
|
||||
}
|
||||
|
||||
public function testColor()
|
||||
public function testWorkContract(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$user->setWorkHoursMonday(7200);
|
||||
self::assertTrue($user->hasWorkHourConfiguration());
|
||||
$user->setWorkHoursTuesday(7300);
|
||||
$user->setWorkHoursWednesday(7400);
|
||||
$user->setWorkHoursThursday(7500);
|
||||
$user->setWorkHoursFriday(7600);
|
||||
$user->setWorkHoursSaturday(7700);
|
||||
$user->setWorkHoursSunday(7800);
|
||||
$user->setHolidaysPerYear(10);
|
||||
self::assertTrue($user->hasWorkHourConfiguration());
|
||||
|
||||
self::assertEquals(7200, $user->getWorkHoursMonday());
|
||||
self::assertEquals(7300, $user->getWorkHoursTuesday());
|
||||
self::assertEquals(7400, $user->getWorkHoursWednesday());
|
||||
self::assertEquals(7500, $user->getWorkHoursThursday());
|
||||
self::assertEquals(7600, $user->getWorkHoursFriday());
|
||||
self::assertEquals(7700, $user->getWorkHoursSaturday());
|
||||
self::assertEquals(7800, $user->getWorkHoursSunday());
|
||||
self::assertEquals(10, $user->getHolidaysPerYear());
|
||||
|
||||
self::assertEquals(7200, $user->getWorkHoursForDay(new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
self::assertEquals(7300, $user->getWorkHoursForDay(new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
self::assertEquals(7400, $user->getWorkHoursForDay(new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
self::assertEquals(7500, $user->getWorkHoursForDay(new \DateTime('2023-05-11 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
self::assertEquals(7600, $user->getWorkHoursForDay(new \DateTime('2023-05-12 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
self::assertEquals(7700, $user->getWorkHoursForDay(new \DateTime('2023-05-13 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
self::assertEquals(7800, $user->getWorkHoursForDay(new \DateTime('2023-05-14 12:00:00', new \DateTimeZone('Europe/Berlin'))));
|
||||
}
|
||||
|
||||
public function testColor(): void
|
||||
{
|
||||
$sut = new User();
|
||||
self::assertNull($sut->getColor());
|
||||
|
||||
54
tests/Entity/WorkingTimeTest.php
Normal file
54
tests/Entity/WorkingTimeTest.php
Normal 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\Tests\Entity;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\WorkingTime
|
||||
*/
|
||||
class WorkingTimeTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setUsername('bar');
|
||||
$date = new \DateTime();
|
||||
$sut = new WorkingTime($user, $date);
|
||||
|
||||
self::assertSame($user, $sut->getUser());
|
||||
self::assertSame($date, $sut->getDate());
|
||||
|
||||
self::assertNull($sut->getId());
|
||||
|
||||
self::assertEquals(0, $sut->getActualTime());
|
||||
self::assertEquals(0, $sut->getExpectedTime());
|
||||
self::assertNull($sut->getApprovedAt());
|
||||
self::assertNull($sut->getApprovedBy());
|
||||
self::assertFalse($sut->isApproved());
|
||||
|
||||
$approvedAt = new \DateTime('2023-01-01 12:00:00', new \DateTimeZone('Europe/Vienna'));
|
||||
$approvedBy = new User();
|
||||
$approvedBy->setUsername('foo');
|
||||
|
||||
$sut->setApprovedAt($approvedAt);
|
||||
$sut->setApprovedBy($approvedBy);
|
||||
$sut->setActualTime(5600);
|
||||
$sut->setExpectedTime(8600);
|
||||
|
||||
self::assertSame(5600, $sut->getActualTime());
|
||||
self::assertSame(8600, $sut->getExpectedTime());
|
||||
self::assertSame($approvedAt, $sut->getApprovedAt());
|
||||
self::assertSame($approvedBy, $sut->getApprovedBy());
|
||||
self::assertTrue($sut->isApproved());
|
||||
}
|
||||
}
|
||||
36
tests/Event/WorkContractDetailControllerEventTest.php
Normal file
36
tests/Event/WorkContractDetailControllerEventTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkContractDetailControllerEvent;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\WorkContractDetailControllerEvent
|
||||
*/
|
||||
class WorkContractDetailControllerEventTest extends TestCase
|
||||
{
|
||||
public function testGetter(): void
|
||||
{
|
||||
$user = new User();
|
||||
$date = new \DateTime('2023-02-10');
|
||||
$year = new Year($date, $user);
|
||||
$sut = new WorkContractDetailControllerEvent($year);
|
||||
|
||||
self::assertSame($year, $sut->getYear());
|
||||
self::assertEquals([], $sut->getController());
|
||||
|
||||
$sut->addController('foo');
|
||||
$sut->addController('bar');
|
||||
self::assertEquals(['foo', 'bar'], $sut->getController());
|
||||
}
|
||||
}
|
||||
43
tests/Event/WorkingTimeYearSummaryEventTest.php
Normal file
43
tests/Event/WorkingTimeYearSummaryEventTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use App\WorkingTime\Model\YearSummary;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\WorkingTimeYearSummaryEvent
|
||||
*/
|
||||
class WorkingTimeYearSummaryEventTest extends TestCase
|
||||
{
|
||||
public function testGetter(): void
|
||||
{
|
||||
$user = new User();
|
||||
$date = new \DateTime('2023-02-10');
|
||||
$year = new Year($date, $user);
|
||||
$until = new \DateTimeImmutable();
|
||||
$yearPerUser = new YearPerUserSummary($year);
|
||||
$sut = new WorkingTimeYearSummaryEvent($yearPerUser, $until);
|
||||
|
||||
self::assertSame($year, $sut->getYear());
|
||||
self::assertSame($until, $sut->getUntil());
|
||||
|
||||
$month = new \DateTime('2023-04-10');
|
||||
$holiday = new YearSummary($month, 'holiday');
|
||||
$sut->addSummary($holiday);
|
||||
$sickness = new YearSummary($month, 'sickness');
|
||||
$sut->addSummary($sickness);
|
||||
self::assertEquals([$holiday, $sickness], $yearPerUser->getSummaries());
|
||||
}
|
||||
}
|
||||
26
tests/Model/DayTest.php
Normal file
26
tests/Model/DayTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Model\Day;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Model\Day
|
||||
*/
|
||||
class DayTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$date = new \DateTimeImmutable('2023-07-21');
|
||||
$sut = new Day($date);
|
||||
self::assertSame($date, $sut->getDay());
|
||||
}
|
||||
}
|
||||
32
tests/Model/MonthTest.php
Normal file
32
tests/Model/MonthTest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Model\Month;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Model\Month
|
||||
*/
|
||||
class MonthTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$months = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
foreach($months as $key => $days) {
|
||||
$index = ++$key;
|
||||
$monthKey = ($index < 10) ? '0' . $index : $index;
|
||||
$date = new \DateTimeImmutable(sprintf('2020-%s-25 13:00:00', $monthKey));
|
||||
$month = new Month($date);
|
||||
self::assertEquals(sprintf('2020-%s-25', $monthKey), $month->getMonth()->format('Y-m-d'));
|
||||
self::assertCount($days, $month->getDays());
|
||||
}
|
||||
}
|
||||
}
|
||||
35
tests/Model/YearTest.php
Normal file
35
tests/Model/YearTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Model\Year;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Model\Year
|
||||
*/
|
||||
class YearTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$date = new \DateTimeImmutable('2020-03-14 13:00:00');
|
||||
$sut = new Year($date);
|
||||
self::assertCount(12, $sut->getMonths());
|
||||
self::assertSame($date, $sut->getYear());
|
||||
$months = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
foreach($months as $key => $days) {
|
||||
$index = ++$key;
|
||||
$monthKey = ($index < 10) ? '0' . $index : $index;
|
||||
$month = $sut->getMonth(new \DateTimeImmutable(sprintf('2020-%s-13 13:00:00', $monthKey)));
|
||||
self::assertEquals(sprintf('2020-%s-01', $monthKey), $month->getMonth()->format('Y-m-d'));
|
||||
self::assertCount($days, $month->getDays());
|
||||
}
|
||||
}
|
||||
}
|
||||
37
tests/WorkingTime/Model/BoxConfigurationTest.php
Normal file
37
tests/WorkingTime/Model/BoxConfigurationTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\WorkingTime\Model\BoxConfiguration;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\BoxConfiguration
|
||||
*/
|
||||
class BoxConfigurationTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$sut = new BoxConfiguration();
|
||||
self::assertFalse($sut->isDecimal());
|
||||
self::assertFalse($sut->isCollapsed());
|
||||
}
|
||||
|
||||
public function testSetter(): void
|
||||
{
|
||||
$sut = new BoxConfiguration();
|
||||
$sut->setDecimal(true);
|
||||
self::assertTrue($sut->isDecimal());
|
||||
self::assertFalse($sut->isCollapsed());
|
||||
$sut->setCollapsed(true);
|
||||
self::assertTrue($sut->isDecimal());
|
||||
self::assertTrue($sut->isCollapsed());
|
||||
}
|
||||
}
|
||||
35
tests/WorkingTime/Model/DayTest.php
Normal file
35
tests/WorkingTime/Model/DayTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\WorkingTime\Model\Day;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\Day
|
||||
*/
|
||||
class DayTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$sut = new Day(new \DateTimeImmutable());
|
||||
self::assertNull($sut->getWorkingTime());
|
||||
}
|
||||
|
||||
public function testSetter(): void
|
||||
{
|
||||
$sut = new Day(new \DateTimeImmutable());
|
||||
$workingTime = new WorkingTime(new User(), new \DateTimeImmutable());
|
||||
$sut->setWorkingTime($workingTime);
|
||||
self::assertSame($workingTime, $sut->getWorkingTime());
|
||||
}
|
||||
}
|
||||
35
tests/WorkingTime/Model/MonthSummaryTest.php
Normal file
35
tests/WorkingTime/Model/MonthSummaryTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\WorkingTime\Model\MonthSummary;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\MonthSummary
|
||||
*/
|
||||
class MonthSummaryTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$sut = new MonthSummary(new \DateTimeImmutable());
|
||||
self::assertEquals(0, $sut->getExpectedTime());
|
||||
self::assertEquals(0, $sut->getActualTime());
|
||||
}
|
||||
|
||||
public function testSetter(): void
|
||||
{
|
||||
$sut = new MonthSummary(new \DateTimeImmutable());
|
||||
$sut->setExpectedTime(98765);
|
||||
$sut->setActualTime(123456);
|
||||
self::assertEquals(98765, $sut->getExpectedTime());
|
||||
self::assertEquals(123456, $sut->getActualTime());
|
||||
}
|
||||
}
|
||||
56
tests/WorkingTime/Model/MonthTest.php
Normal file
56
tests/WorkingTime/Model/MonthTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\WorkingTime\Model\Day;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\Month
|
||||
*/
|
||||
class MonthTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$months = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
foreach($months as $key => $days) {
|
||||
$index = ++$key;
|
||||
$monthKey = ($index < 10) ? '0' . $index : $index;
|
||||
$date = new \DateTimeImmutable(sprintf('2020-%s-25 13:00:00', $monthKey));
|
||||
$month = new Month($date);
|
||||
self::assertEquals(sprintf('2020-%s-25', $monthKey), $month->getMonth()->format('Y-m-d'));
|
||||
self::assertCount($days, $month->getDays());
|
||||
self::assertTrue($month->isLocked());
|
||||
self::assertEquals(0, $month->getActualTime());
|
||||
self::assertEquals(0, $month->getExpectedTime(new \DateTimeImmutable('2035-01-02')));
|
||||
self::assertNull($month->getLockDate());
|
||||
self::assertNull($month->getLockedBy());
|
||||
foreach ($month->getDays() as $day) {
|
||||
self::assertInstanceOf(Day::class, $day);
|
||||
self::assertNull($day->getWorkingTime());
|
||||
|
||||
$duration = rand(0, 28000);
|
||||
$expected = rand(0, 28000);
|
||||
|
||||
$workingTime = new WorkingTime(new User(), $day->getDay());
|
||||
$workingTime->setExpectedTime($expected);
|
||||
$workingTime->setActualTime($duration);
|
||||
$day->setWorkingTime($workingTime);
|
||||
|
||||
self::assertSame($workingTime, $day->getWorkingTime());
|
||||
self::assertEquals($expected, $day->getWorkingTime()->getExpectedTime());
|
||||
self::assertEquals($duration, $day->getWorkingTime()->getActualTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
tests/WorkingTime/Model/YearPerUserSummaryTest.php
Normal file
65
tests/WorkingTime/Model/YearPerUserSummaryTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use App\WorkingTime\Model\YearSummary;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\YearPerUserSummary
|
||||
*/
|
||||
class YearPerUserSummaryTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable();
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('foooo');
|
||||
$year = new Year($monthDate, $user);
|
||||
$sut = new YearPerUserSummary($year);
|
||||
|
||||
self::assertSame($year, $sut->getYear());
|
||||
self::assertSame($user, $sut->getUser());
|
||||
self::assertEquals(0, $sut->getExpectedTime());
|
||||
self::assertEquals(0, $sut->getActualTime());
|
||||
self::assertEquals(0, $sut->count());
|
||||
self::assertEquals([], $sut->getSummaries());
|
||||
}
|
||||
|
||||
public function testWithSummary(): void
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable();
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('foooo');
|
||||
$year = new Year($monthDate, $user);
|
||||
$sut = new YearPerUserSummary($year);
|
||||
|
||||
$summary = new YearSummary($monthDate, 'Foo-Bar');
|
||||
|
||||
$monthSummary = $summary->getMonth(new \DateTimeImmutable('2020-04-27'));
|
||||
$monthSummary->setActualTime(35679);
|
||||
$monthSummary->setExpectedTime(135679);
|
||||
|
||||
$monthSummary = $summary->getMonth(new \DateTimeImmutable('2020-07-27'));
|
||||
$monthSummary->setActualTime(95679);
|
||||
$monthSummary->setExpectedTime(235679);
|
||||
|
||||
$sut->addSummary($summary);
|
||||
|
||||
self::assertEquals(131358, $sut->getActualTime());
|
||||
self::assertEquals(371358, $sut->getExpectedTime());
|
||||
self::assertEquals(1, $sut->count());
|
||||
self::assertEquals([$summary], $sut->getSummaries());
|
||||
self::assertInstanceOf(\Traversable::class, $sut->getIterator());
|
||||
}
|
||||
}
|
||||
45
tests/WorkingTime/Model/YearSummaryTest.php
Normal file
45
tests/WorkingTime/Model/YearSummaryTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\WorkingTime\Model\YearSummary;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\YearSummary
|
||||
*/
|
||||
class YearSummaryTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable();
|
||||
$sut = new YearSummary($monthDate, 'Foo-Bar');
|
||||
self::assertEquals('Foo-Bar', $sut->getTitle());
|
||||
self::assertEquals(0, $sut->getExpectedTime());
|
||||
self::assertEquals(0, $sut->getActualTime());
|
||||
}
|
||||
|
||||
public function testWithSummary(): void
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable('2020-01-01');
|
||||
$sut = new YearSummary($monthDate, 'Foo-Bar');
|
||||
|
||||
$monthSummary = $sut->getMonth(new \DateTimeImmutable('2020-04-27'));
|
||||
$monthSummary->setActualTime(35679);
|
||||
$monthSummary->setExpectedTime(135679);
|
||||
|
||||
$monthSummary = $sut->getMonth(new \DateTimeImmutable('2020-07-27'));
|
||||
$monthSummary->setActualTime(95679);
|
||||
$monthSummary->setExpectedTime(235679);
|
||||
|
||||
self::assertEquals(131358, $sut->getActualTime());
|
||||
self::assertEquals(371358, $sut->getExpectedTime());
|
||||
}
|
||||
}
|
||||
70
tests/WorkingTime/Model/YearTest.php
Normal file
70
tests/WorkingTime/Model/YearTest.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\Year
|
||||
*/
|
||||
class YearTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$year = new \DateTimeImmutable('2020-02-02');
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('foooo');
|
||||
$sut = new Year($year, $user);
|
||||
|
||||
$until = new \DateTimeImmutable('2020-09-25');
|
||||
|
||||
self::assertSame($user, $sut->getUser());
|
||||
self::assertEquals(0, $sut->getActualTime());
|
||||
self::assertEquals(0, $sut->getExpectedTime($until));
|
||||
}
|
||||
|
||||
public function testWithSummary(): void
|
||||
{
|
||||
$year = new \DateTimeImmutable('2020-02-02');
|
||||
$user = new User();
|
||||
$user->setUserIdentifier('foooo');
|
||||
$sut = new Year($year, $user);
|
||||
|
||||
$until = new \DateTimeImmutable('2020-09-25');
|
||||
|
||||
self::assertSame($user, $sut->getUser());
|
||||
self::assertEquals(0, $sut->getActualTime());
|
||||
self::assertEquals(0, $sut->getExpectedTime($until));
|
||||
|
||||
$workingTime = new WorkingTime(new User(), new \DateTimeImmutable('2020-04-14'));
|
||||
$workingTime->setExpectedTime(54321);
|
||||
$workingTime->setActualTime(12345);
|
||||
$day = $sut->getMonth(new \DateTimeImmutable('2020-04-25'))->getDays()[13];
|
||||
$day->setWorkingTime($workingTime);
|
||||
|
||||
self::assertEquals(12345, $sut->getActualTime());
|
||||
self::assertEquals(54321, $sut->getExpectedTime($until));
|
||||
|
||||
$workingTime = new WorkingTime(new User(), new \DateTimeImmutable('2020-06-20'));
|
||||
$workingTime->setExpectedTime(9843);
|
||||
$workingTime->setActualTime(777);
|
||||
$day = $sut->getMonth(new \DateTimeImmutable('2020-06-25'))->getDays()[19];
|
||||
$day->setWorkingTime($workingTime);
|
||||
|
||||
self::assertEquals(13122, $sut->getActualTime());
|
||||
self::assertEquals(64164, $sut->getExpectedTime($until));
|
||||
|
||||
self::assertEquals(54321, $sut->getExpectedTime(new \DateTimeImmutable('2020-04-25')));
|
||||
self::assertEquals(0, $sut->getExpectedTime(new \DateTimeImmutable('2020-03-25')));
|
||||
}
|
||||
}
|
||||
@@ -4782,11 +4782,6 @@ parameters:
|
||||
count: 1
|
||||
path: Entity/UserTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testColor\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Entity/UserTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\UserTest\\:\\:testDatetime\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -267,6 +267,18 @@
|
||||
<target>Das Verzeichnis „%dir%“ ist schreibgeschützt.</target>
|
||||
</trans-unit>
|
||||
<!-- General labels -->
|
||||
<trans-unit id="EYXzfTO" resname="deleted">
|
||||
<source>deleted</source>
|
||||
<target>Gelöscht</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="EX27Gdt" resname="created_by">
|
||||
<source>created_by</source>
|
||||
<target>Erstellt von</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6Hip2aF" resname="created_at">
|
||||
<source>created_at</source>
|
||||
<target>Erstellt am</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="DodjLNR" resname="date">
|
||||
<source>date</source>
|
||||
<target>Datum</target>
|
||||
@@ -591,6 +603,86 @@
|
||||
<source>profile.roles</source>
|
||||
<target>Rollen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eCEGRzN" resname="work_contract">
|
||||
<source>work_contract</source>
|
||||
<target>Arbeitsvertrag</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7_d7IB3" resname="evaluation">
|
||||
<source>evaluation</source>
|
||||
<target>Auswertung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4lrfIsZ" resname="complete_month">
|
||||
<source>complete_month</source>
|
||||
<target>Monat abschliessen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="W8XQTkJ" resname="complete_month.help">
|
||||
<source>complete_month.help</source>
|
||||
<target>Sobald der Monat abgeschlossen ist, kann der Benutzer für diesen Zeitraum keine Zeiten mehr anlegen oder bearbeiten.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eWmotjb" resname="completed_month_pdf">
|
||||
<source>completed_month_pdf</source>
|
||||
<target>Monats-Abschluss PDF</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AXr_aHI" resname="completed_month_pdf_note">
|
||||
<source>completed_month_pdf_note</source>
|
||||
<target>Dieses Dokument dient als Nachweis der geleisteten Arbeitszeiten.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yXrOTI." resname="review">
|
||||
<source>review</source>
|
||||
<target>Überprüfung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mS4SSmt" resname="work_times">
|
||||
<source>work_times</source>
|
||||
<target>Arbeitszeiten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="iaJj4va" resname="work_times_account">
|
||||
<source>work_times_account</source>
|
||||
<target>Arbeitszeitkonto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1btzNDa" resname="work_times_should">
|
||||
<source>work_times_should</source>
|
||||
<target>Soll-Stunden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="PE2iiaW" resname="work_times_should.help">
|
||||
<source>work_times_should.help</source>
|
||||
<target>Die hier konfigurierten Werte werden zur Berechnung des Stundenkontos verwendet.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
|
||||
<source>work_times_should.none_configured</source>
|
||||
<target>Für diesen Benutzer wurden in den Arbeitsvertragseinstellungen keine Sollstunden konfiguriert.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="31dFXhR" resname="work_times_is">
|
||||
<source>work_times_is</source>
|
||||
<target>Ist-Stunden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target>Geleistete Stunden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target>Bestätigt von %user% am %date%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wZRRqg0" resname="manual_bookings">
|
||||
<source>manual_bookings</source>
|
||||
<target>Manuelle Buchungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IalLD5Q" resname="manual_bookings.work_contract_intro">
|
||||
<source>manual_bookings.work_contract_intro</source>
|
||||
<target>Manuelle Buchungen können weder nachträglich geändert noch gelöscht werden!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="znIJiOw" resname="manual_bookings.duration_help">
|
||||
<source>manual_bookings.duration_help</source>
|
||||
<target>Die Dauer kann sowohl positiv (Erhöhung der Ist-Zeit) als auch negativ (Reduzierung der Ist-Zeit) sein.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="M.zFjwe" resname="pto_year_holiday">
|
||||
<source>pto_year_holiday</source>
|
||||
<target>Urlaubstage pro Jahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="gcFtM3o" resname="holiday">
|
||||
<source>holiday</source>
|
||||
<target>Urlaub</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="MQKiG33" resname="profile.preferences">
|
||||
<source>profile.preferences</source>
|
||||
<target>Einstellungen</target>
|
||||
|
||||
@@ -267,6 +267,18 @@
|
||||
<target>Directory "%dir%" is write protected.</target>
|
||||
</trans-unit>
|
||||
<!-- General labels -->
|
||||
<trans-unit id="EYXzfTO" resname="deleted">
|
||||
<source>deleted</source>
|
||||
<target>Deleted</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="EX27Gdt" resname="created_by">
|
||||
<source>created_by</source>
|
||||
<target>Created by</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="6Hip2aF" resname="created_at">
|
||||
<source>created_at</source>
|
||||
<target>Created at</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="DodjLNR" resname="date">
|
||||
<source>date</source>
|
||||
<target>Date</target>
|
||||
@@ -591,6 +603,86 @@
|
||||
<source>profile.roles</source>
|
||||
<target>Roles</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eCEGRzN" resname="work_contract">
|
||||
<source>work_contract</source>
|
||||
<target>Employment contract</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="7_d7IB3" resname="evaluation">
|
||||
<source>evaluation</source>
|
||||
<target>Evaluation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="4lrfIsZ" resname="complete_month">
|
||||
<source>complete_month</source>
|
||||
<target>Complete month</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="W8XQTkJ" resname="complete_month.help">
|
||||
<source>complete_month.help</source>
|
||||
<target>After completing the month, the user will no longer be able to create or edit times for that period.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="eWmotjb" resname="completed_month_pdf">
|
||||
<source>completed_month_pdf</source>
|
||||
<target>Completed month PDF</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="AXr_aHI" resname="completed_month_pdf_note">
|
||||
<source>completed_month_pdf_note</source>
|
||||
<target>This document serves as proof of the hours worked.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yXrOTI." resname="review">
|
||||
<source>review</source>
|
||||
<target>Review</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="mS4SSmt" resname="work_times">
|
||||
<source>work_times</source>
|
||||
<target>Working times</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="iaJj4va" resname="work_times_account">
|
||||
<source>work_times_account</source>
|
||||
<target>Working time account</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="1btzNDa" resname="work_times_should">
|
||||
<source>work_times_should</source>
|
||||
<target>Target-hours</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="PE2iiaW" resname="work_times_should.help">
|
||||
<source>work_times_should.help</source>
|
||||
<target>The values configured here are used to calculate the hourly account.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
|
||||
<source>work_times_should.none_configured</source>
|
||||
<target>No target hours have been configured for this user in the employment contract settings.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="31dFXhR" resname="work_times_is">
|
||||
<source>work_times_is</source>
|
||||
<target>Is-hours</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="uHrZlbq" resname="work_times_result">
|
||||
<source>work_times_result</source>
|
||||
<target>Hours worked</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
|
||||
<source>confirmed_by_at</source>
|
||||
<target>Confirmed by %user% at %date%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="wZRRqg0" resname="manual_bookings">
|
||||
<source>manual_bookings</source>
|
||||
<target>Manual bookings</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IalLD5Q" resname="manual_bookings.work_contract_intro">
|
||||
<source>manual_bookings.work_contract_intro</source>
|
||||
<target>Manual bookings can neither be subsequently changed nor deleted!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="znIJiOw" resname="manual_bookings.duration_help">
|
||||
<source>manual_bookings.duration_help</source>
|
||||
<target>The duration can be both positive (increase of the is-time) and negative (reduction of the is-time).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="M.zFjwe" resname="pto_year_holiday">
|
||||
<source>pto_year_holiday</source>
|
||||
<target>Urlaubstage pro Jahr</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="gcFtM3o" resname="holiday">
|
||||
<source>holiday</source>
|
||||
<target>Holiday</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="MQKiG33" resname="profile.preferences">
|
||||
<source>profile.preferences</source>
|
||||
<target>Preferences</target>
|
||||
|
||||
Reference in New Issue
Block a user