mirror of
https://github.com/kevinpapst/kimai2.git
synced 2025-03-17 14:32:38 +00:00
added new report: project view (#1738)
This commit is contained in:
parent
0b7d551048
commit
c34e9f576d
28 changed files with 892 additions and 83 deletions
assets
public/build
src
templates
tests
Command
Controller/Reporting
Reporting
Twig/Runtime
translations
|
@ -44,7 +44,7 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
|
|||
});
|
||||
for (let checkbox of this.modal.querySelectorAll('form input[type=checkbox]')) {
|
||||
checkbox.addEventListener('click', function () {
|
||||
self.changeVisibility(checkbox.getAttribute('name'));
|
||||
self.changeVisibility(checkbox.getAttribute('name'), checkbox.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
|
|||
jQuery(this.modal).modal('toggle');
|
||||
}
|
||||
|
||||
changeVisibility(columnName) {
|
||||
changeVisibility(columnName, checked) {
|
||||
const table = document.getElementById('datatable_' + this.id).getElementsByClassName('dataTable')[0];
|
||||
let column = 0;
|
||||
let foundColumn = false;
|
||||
|
@ -79,6 +79,11 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
|
|||
foundColumn = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (columnElement.getAttribute('colspan') !== null) {
|
||||
console.log('Tables with colspans are not supported!');
|
||||
}
|
||||
|
||||
column++;
|
||||
}
|
||||
|
||||
|
@ -87,7 +92,15 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
|
|||
}
|
||||
|
||||
for (let rowElement of table.getElementsByTagName('tr')) {
|
||||
rowElement.children[column].classList.toggle('hidden');
|
||||
if (rowElement.children[column] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
rowElement.children[column].classList.remove('hidden');
|
||||
} else {
|
||||
rowElement.children[column].classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,3 +4,10 @@
|
|||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.form-reporting .box-header .form-group {
|
||||
margin-right: 10px;
|
||||
label {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,10 +5,10 @@
|
|||
"build/runtime.098eaae1.js",
|
||||
"build/0.79dbdbb9.js",
|
||||
"build/1.32489d92.js",
|
||||
"build/app.b2c3295c.js"
|
||||
"build/app.f451ae28.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.fe8aa88b.css"
|
||||
"build/app.7c0ca7bb.css"
|
||||
]
|
||||
},
|
||||
"invoice": {
|
||||
|
@ -53,8 +53,8 @@
|
|||
"build/runtime.098eaae1.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.79dbdbb9.js": "sha384-U2Ao0ORAZ8PCeDmyRsqQFET3hc7pfUBimq0PrqFdG4/s0Bdi+qBj4TJK3o70bCd5",
|
||||
"build/1.32489d92.js": "sha384-wVkjh5FzjFhMV4S4uNP23E/OLBOf+Zi7t3lpm9eWzoMr/tm2pydT+q0Op1XHuoUP",
|
||||
"build/app.b2c3295c.js": "sha384-lqEgWnKWvGpWM7J8WsMYvPZv8+EYdwCG5T9pHtwdLerTh9wlmYyXGfFCMbc/iVS6",
|
||||
"build/app.fe8aa88b.css": "sha384-xmgEZuf0VPsvPdzU/yqK8qL6W+d+d6LJn0e0oD66LYnCxs5I44d1mp1bAtRN905Q",
|
||||
"build/app.f451ae28.js": "sha384-uFHnZPiRrWKf2q8N4zNE1YguKVtOXbES0+rKlehQAj7bIDvCHOcpVo8djMU6VtE6",
|
||||
"build/app.7c0ca7bb.css": "sha384-0I0CpC2eA9LQewNIo/c6Qkhra9sOsqadi0bi9DfC2YWQxBi+ZRYAl4BVt9jMOyGE",
|
||||
"build/invoice.74279541.js": "sha384-2BXic5Sgorf2tXai6zSAN4wLY2dbg06L03/xMKW6itMcszvtnRArKzfBh6DNcF3f",
|
||||
"build/invoice.13d8ef4e.css": "sha384-B6RN/wZJToSBCZk2JeLokIqWEhbh+Eb9arYbt9dM+YoC2Z6PnCeTwTqSGyexWWJh",
|
||||
"build/invoice-pdf.0efd7a97.js": "sha384-bSdIeRCtEJiYYuc2reb0e5CpJ1Kbd1lQNEkElMTiq1SX0IINzdwJJYf6WnCcHrNC",
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"build/0.79dbdbb9.js": "build/0.79dbdbb9.js",
|
||||
"build/1.32489d92.js": "build/1.32489d92.js",
|
||||
"build/2.7ab75d0a.js": "build/2.7ab75d0a.js",
|
||||
"build/app.css": "build/app.fe8aa88b.css",
|
||||
"build/app.js": "build/app.b2c3295c.js",
|
||||
"build/app.css": "build/app.7c0ca7bb.css",
|
||||
"build/app.js": "build/app.f451ae28.js",
|
||||
"build/calendar.css": "build/calendar.1408f57e.css",
|
||||
"build/calendar.js": "build/calendar.070aab88.js",
|
||||
"build/chart.js": "build/chart.34d60a88.js",
|
||||
|
|
52
src/Controller/Reporting/ProjectViewController.php
Normal file
52
src/Controller/Reporting/ProjectViewController.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?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\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Reporting\ProjectView\ProjectViewForm;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use App\Reporting\ProjectView\ProjectViewService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectViewController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_view", name="report_project_view", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('budget_project')")
|
||||
*/
|
||||
public function __invoke(Request $request, ProjectViewService $service)
|
||||
{
|
||||
$query = new ProjectViewQuery($this->getDateTimeFactory()->createDateTime(), $this->getUser());
|
||||
|
||||
$form = $this->createForm(ProjectViewForm::class, $query, [
|
||||
'action' => $this->generateUrl('report_project_view')
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$entries = $service->getProjectView($query);
|
||||
|
||||
$byCustomer = [];
|
||||
foreach ($entries as $entry) {
|
||||
$customer = $entry->getProject()->getCustomer();
|
||||
if (!isset($byCustomer[$customer->getId()])) {
|
||||
$byCustomer[$customer->getId()] = ['customer' => $customer, 'projects' => []];
|
||||
}
|
||||
$byCustomer[$customer->getId()]['projects'][] = $entry;
|
||||
}
|
||||
|
||||
return $this->render('reporting/project_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
61
src/Reporting/ProjectView/ProjectViewForm.php
Normal file
61
src/Reporting/ProjectView/ProjectViewForm.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?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\Reporting\ProjectView;
|
||||
|
||||
use App\Form\Type\CustomerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectViewForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'width' => false,
|
||||
]);
|
||||
$builder->add('includeNoBudget', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoBudget',
|
||||
]);
|
||||
$builder->add('includeNoWork', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoWork',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectViewQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
128
src/Reporting/ProjectView/ProjectViewModel.php
Normal file
128
src/Reporting/ProjectView/ProjectViewModel.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?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\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Project;
|
||||
|
||||
final class ProjectViewModel
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationDay = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationWeek = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationMonth = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationTotal = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $rateTotal = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $notExportedDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $notExportedRate = 0.00;
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getDurationDay(): int
|
||||
{
|
||||
return $this->durationDay;
|
||||
}
|
||||
|
||||
public function setDurationDay(int $durationDay): void
|
||||
{
|
||||
$this->durationDay = $durationDay;
|
||||
}
|
||||
|
||||
public function getDurationWeek(): int
|
||||
{
|
||||
return $this->durationWeek;
|
||||
}
|
||||
|
||||
public function setDurationWeek(int $durationWeek): void
|
||||
{
|
||||
$this->durationWeek = $durationWeek;
|
||||
}
|
||||
|
||||
public function getDurationMonth(): int
|
||||
{
|
||||
return $this->durationMonth;
|
||||
}
|
||||
|
||||
public function setDurationMonth(int $durationMonth): void
|
||||
{
|
||||
$this->durationMonth = $durationMonth;
|
||||
}
|
||||
|
||||
public function getDurationTotal(): int
|
||||
{
|
||||
return $this->durationTotal;
|
||||
}
|
||||
|
||||
public function setDurationTotal(int $durationTotal): void
|
||||
{
|
||||
$this->durationTotal = $durationTotal;
|
||||
}
|
||||
|
||||
public function getNotExportedDuration(): int
|
||||
{
|
||||
return $this->notExportedDuration;
|
||||
}
|
||||
|
||||
public function setNotExportedDuration(int $notExportedDuration): void
|
||||
{
|
||||
$this->notExportedDuration = $notExportedDuration;
|
||||
}
|
||||
|
||||
public function getNotExportedRate(): float
|
||||
{
|
||||
return $this->notExportedRate;
|
||||
}
|
||||
|
||||
public function setNotExportedRate(float $notExportedRate): void
|
||||
{
|
||||
$this->notExportedRate = $notExportedRate;
|
||||
}
|
||||
|
||||
public function getRateTotal(): int
|
||||
{
|
||||
return $this->rateTotal;
|
||||
}
|
||||
|
||||
public function setRateTotal(int $rateTotal): void
|
||||
{
|
||||
$this->rateTotal = $rateTotal;
|
||||
}
|
||||
}
|
84
src/Reporting/ProjectView/ProjectViewQuery.php
Normal file
84
src/Reporting/ProjectView/ProjectViewQuery.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?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\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectViewQuery
|
||||
{
|
||||
/**
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $today;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoBudget = false;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoWork = false;
|
||||
|
||||
public function __construct(DateTime $today, User $user)
|
||||
{
|
||||
$this->today = $today;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function isIncludeNoBudget(): bool
|
||||
{
|
||||
return $this->includeNoBudget;
|
||||
}
|
||||
|
||||
public function setIncludeNoBudget(bool $includeNoBudget): void
|
||||
{
|
||||
$this->includeNoBudget = $includeNoBudget;
|
||||
}
|
||||
|
||||
public function isIncludeNoWork(): bool
|
||||
{
|
||||
return $this->includeNoWork;
|
||||
}
|
||||
|
||||
public function setIncludeNoWork(bool $includeNoWork): void
|
||||
{
|
||||
$this->includeNoWork = $includeNoWork;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): void
|
||||
{
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function getToday(): DateTime
|
||||
{
|
||||
return $this->today;
|
||||
}
|
||||
}
|
157
src/Reporting/ProjectView/ProjectViewService.php
Normal file
157
src/Reporting/ProjectView/ProjectViewService.php
Normal file
|
@ -0,0 +1,157 @@
|
|||
<?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\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Exception;
|
||||
|
||||
final class ProjectViewService
|
||||
{
|
||||
private $repository;
|
||||
private $timesheetRepository;
|
||||
|
||||
public function __construct(ProjectRepository $projectRepository, TimesheetRepository $timesheetRepository)
|
||||
{
|
||||
$this->repository = $projectRepository;
|
||||
$this->timesheetRepository = $timesheetRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return ProjectViewModel[]
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getProjectView(ProjectViewQuery $query): array
|
||||
{
|
||||
$factory = new DateTimeFactory($query->getToday()->getTimezone());
|
||||
$user = $query->getUser();
|
||||
$today = clone $query->getToday();
|
||||
|
||||
$begin = $factory->getStartOfWeek($today);
|
||||
$end = $factory->getEndOfWeek($today);
|
||||
$startMonth = (clone $begin)->modify('first day of this month');
|
||||
$endMonth = (clone $begin)->modify('last day of this month');
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p AS project')
|
||||
->addSelect('SUM(t.duration) AS totalDuration')
|
||||
->addSelect('SUM(t.rate) AS totalRate')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->leftJoin(Timesheet::class, 't', 'WITH', 'p.id = t.project')
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->addGroupBy('p')
|
||||
->addGroupBy('t.project')
|
||||
;
|
||||
|
||||
if ($query->getCustomer() !== null) {
|
||||
$qb->andWhere($qb->expr()->eq('c', ':customer'));
|
||||
$qb->setParameter('customer', $query->getCustomer()->getId());
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoWork()) {
|
||||
$qb->andHaving($qb->expr()->gt('totalDuration', 0));
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoBudget()) {
|
||||
$qb->andWhere($qb->expr()->gt('p.timeBudget', 0));
|
||||
}
|
||||
|
||||
$this->repository->addPermissionCriteria($qb, $user);
|
||||
|
||||
$result = $qb->getQuery()->getResult();
|
||||
|
||||
$projectViews = [];
|
||||
foreach ($result as $res) {
|
||||
$entity = new ProjectViewModel();
|
||||
$entity->setProject($res['project']);
|
||||
$entity->setDurationTotal($res['totalDuration'] ?? 0);
|
||||
$entity->setRateTotal($res['totalRate'] ?? 0);
|
||||
|
||||
$projectViews[$entity->getProject()->getId()] = $entity;
|
||||
}
|
||||
|
||||
$projectIds = array_keys($projectViews);
|
||||
|
||||
// values for today
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) = :starting_date')
|
||||
->groupBy('t.project')
|
||||
->setParameter('starting_date', $today->format('Y-m-d'))
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationDay($row['duration']);
|
||||
}
|
||||
|
||||
// values for the current week
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_date AND :end_date')
|
||||
->groupBy('t.project')
|
||||
->setParameter('start_date', $begin->format('Y-m-d'))
|
||||
->setParameter('end_date', $end->format('Y-m-d'))
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationWeek($row['duration']);
|
||||
}
|
||||
|
||||
// values for the current month
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_month AND :end_month')
|
||||
->groupBy('t.project')
|
||||
->setParameter('start_month', $startMonth)
|
||||
->setParameter('end_month', $endMonth)
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
||||
}
|
||||
|
||||
// values for the all time (not exported)
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration, SUM(t.rate) AS rate')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.exported = :exported')
|
||||
->groupBy('t.project')
|
||||
->setParameter('exported', false, Types::BOOLEAN)
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setNotExportedDuration($row['duration']);
|
||||
$projectViews[$row['id']]->setNotExportedRate($row['rate']);
|
||||
}
|
||||
|
||||
return array_values($projectViews);
|
||||
}
|
||||
}
|
|
@ -11,17 +11,8 @@ namespace App\Reporting;
|
|||
|
||||
final class Report implements ReportInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $label;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function __construct(string $id, string $route, string $label)
|
||||
|
|
|
@ -42,6 +42,9 @@ final class ReportingService
|
|||
if ($this->security->isGranted('view_reporting')) {
|
||||
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week'));
|
||||
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month'));
|
||||
if ($this->security->isGranted('budget_project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view'));
|
||||
}
|
||||
if ($this->security->isGranted('view_other_timesheet')) {
|
||||
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users'));
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ use App\Repository\Paginator\LoaderPaginator;
|
|||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
|
@ -77,7 +78,7 @@ class ProjectRepository extends EntityRepository
|
|||
return $this->count([]);
|
||||
}
|
||||
|
||||
public function getProjectStatistics(Project $project, ?\DateTime $begin = null, ?\DateTime $end = null): ProjectStatistic
|
||||
public function getProjectStatistics(Project $project, ?DateTime $begin = null, ?DateTime $end = null): ProjectStatistic
|
||||
{
|
||||
$stats = new ProjectStatistic($project);
|
||||
|
||||
|
@ -127,7 +128,7 @@ class ProjectRepository extends EntityRepository
|
|||
return $stats;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
|
@ -203,7 +204,7 @@ class ProjectRepository extends EntityRepository
|
|||
$qb->andWhere($qb->expr()->eq('c.visible', ':customer_visible'));
|
||||
|
||||
if (!$query->isIgnoreDate()) {
|
||||
$now = new \DateTime();
|
||||
$now = new DateTime();
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if customer.visible and is_granted('create_project') %}
|
||||
{% if is_granted('view_reporting') and is_granted('budget_project') %}
|
||||
<a class="btn btn-default btn-sm btn-pager" href="{{ path('report_project_view', {'customer': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'report_project_view'|trans({}, 'reporting') }}"><i class="{{ 'reporting'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
<a class="modal-ajax-form open-edit btn btn-default btn-sm btn-pager" data-href="{{ path('admin_project_create_with_customer', {'customer': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
|
||||
{% endif %}
|
||||
{% if projects|length > 0 %}
|
||||
|
|
|
@ -46,22 +46,18 @@
|
|||
{% set orderBy = false %}
|
||||
{% set order = false %}
|
||||
{% endif %}
|
||||
{% set striped = options.striped|default(true) %}
|
||||
{% set striped = options.striped ?? true %}
|
||||
{% set bordered = options.bordered ?? false %}
|
||||
{% set reloadEvent = options.reload|default('') %}
|
||||
{% set translationDomain = options.translationDomain|default('messages') %}
|
||||
{# |default does not work here, as the prefix might be an empty string #}
|
||||
{% set translationPrefix = 'label.' %}
|
||||
{% if options.translationPrefix is defined %}
|
||||
{% set translationPrefix = options.translationPrefix %}
|
||||
{% endif %}
|
||||
{% set translationPrefix = options.translationPrefix ?? 'label.' %}
|
||||
{% set boxClass = options.boxClass ?? 'box box-' ~ admin_lte_context.widget.type ~ ' data_table' %}
|
||||
|
||||
{% import _self as macro %}
|
||||
<div class="box box-{{ admin_lte_context.widget.type }} data_table" id="datatable_{{ tableName }}">
|
||||
<div class="{{ boxClass }}" id="datatable_{{ tableName }}">
|
||||
<div class="box-body no-padding">
|
||||
<div class="dataTables_wrapper form-inline dt-bootstrap">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<table class="table {% if striped %}table-striped {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}" id="dt_{{ tableName }}">
|
||||
<table class="table {% if striped %}table-striped {% endif %}{% if bordered %}table-bordered {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}" id="dt_{{ tableName }}">
|
||||
<thead>
|
||||
<tr>
|
||||
{%- for title, headerOptions in columns -%}
|
||||
|
@ -190,8 +186,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if multi_update_form is not null %}
|
||||
<div id="multi_update_form" style="display: none">
|
||||
{{ form_start(multi_update_form, {'attr': {'data-question': 'update_multiple'|trans}}) }}
|
||||
|
|
118
templates/reporting/project_view.html.twig
Normal file
118
templates/reporting/project_view.html.twig
Normal file
|
@ -0,0 +1,118 @@
|
|||
{% extends 'reporting/layout.html.twig' %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
|
||||
{% block report_title %}{{ 'report_project_view'|trans({}, 'reporting') }}{% endblock %}
|
||||
|
||||
{% set columns = {
|
||||
'name': {'class': 'alwaysVisible'},
|
||||
'today': {'class': 'text-nowrap text-right', 'title': 'daterangepicker.today'|trans({}, 'daterangepicker')},
|
||||
'week': {'class': 'text-nowrap text-right', 'title': 'agendaWeek'|trans},
|
||||
'month': {'class': 'text-nowrap text-right', 'title': 'month'|trans},
|
||||
'durationTotal': {'class': 'hidden-md hidden-sm hidden-xs text-nowrap text-right', 'title': 'stats.durationTotal'|trans},
|
||||
'timeBudget': {'class': 'text-nowrap', 'title': 'label.timeBudget'|trans},
|
||||
'budget': {'class': 'text-nowrap', 'title': 'label.budget'|trans},
|
||||
'stateDuration': {'class': 'hidden-sm hidden-xs text-nowrap text-right', 'title': 'entryState.not_exported'|trans},
|
||||
'stateMoney': {'class': 'hidden-sm hidden-xs text-nowrap text-right', 'title': 'entryState.not_exported'|trans},
|
||||
'projectEnd': {'class': 'hidden-md hidden-sm hidden-xs hidden text-nowrap text-center', 'title': 'label.project_end'|trans},
|
||||
'comment': {'class': 'hidden-md hidden-sm hidden-xs hidden', 'title': 'label.comment'|trans},
|
||||
} %}
|
||||
{% set tableName = 'project_view_reporting' %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block report %}
|
||||
|
||||
{% set hasData = entries|length > 0 %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/progressbar.html.twig" as progress %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% block box_body_class %}project-view-reporting-box {% if hasData %}no-padding{% endif %} table-responsive{% endblock %}
|
||||
{% block box_before %}
|
||||
{{ form_start(form, {'attr': {'class': 'form-inline form-reporting', 'id': 'project-view-form'}}) }}
|
||||
{% endblock %}
|
||||
{% block box_after %}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{{ widgets.action_button('visibility', {'modal': '#modal_project_view_reporting', 'class': 'btn-sm'}) }}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if not hasData %}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, null, {'bordered': true, 'striped': false, 'boxClass': ''}) }}
|
||||
|
||||
{% for id, mapping in entries|sort((a, b) => a.customer.name <=> b.customer.name) %}
|
||||
{% if is_granted('budget', mapping.customer) %}
|
||||
<tr>
|
||||
<th colspan="11">{{ widgets.label_customer(mapping.customer) }}</th>
|
||||
</tr>
|
||||
{% for entry in mapping.projects|sort((a, b) => a.project.name <=> b.project.name) %}
|
||||
{% set project = entry.project %}
|
||||
{% set currency = project.customer.currency %}
|
||||
{% if is_granted('budget', project) %}
|
||||
<tr{% if is_granted('view', project) %} class="alternative-link open-edit" data-href="{{ path('project_details', {'id': project.id}) }}"{% endif %}>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ widgets.label_project(project) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'today') }}">{{ entry.durationDay|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'week') }}">{{ entry.durationWeek|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'month') }}">{{ entry.durationMonth|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'durationTotal') }}">{{ entry.durationTotal|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
|
||||
{% if project.timeBudget > 0 %}
|
||||
{{ progress.progressbar(project.timeBudget, entry.durationTotal|default(0), entry.durationTotal|duration ~ ' / ' ~ project.timeBudget|duration, '') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
|
||||
{% if project.budget > 0 %}
|
||||
{{ progress.progressbar(project.budget, entry.rateTotal|default(0), entry.rateTotal|money(currency) ~ ' / ' ~ project.budget|money(currency), '') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'stateDuration') }}">
|
||||
{% if is_granted('create_export') %}
|
||||
<a href="{{ path('export', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': '', 'preview': true}) }}">
|
||||
{{ entry.notExportedDuration|duration }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ entry.notExportedDuration|duration }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'stateMoney') }}">
|
||||
{% if is_granted('view_invoice') %}
|
||||
<a href="{{ path('invoice', {'customer': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
|
||||
{{ entry.notExportedRate|money(currency) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ entry.notExportedRate|money(currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'projectEnd') }}">{% if project.end is not null %}{{ project.end|date_short }}{% endif %}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ project.comment }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(entries) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
$('#project-view-form').on('change', function(ev) {
|
||||
$(this).submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -12,7 +12,6 @@ namespace App\Tests\Command;
|
|||
use App\Command\InstallCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\InstallCommand
|
||||
|
@ -25,7 +24,7 @@ class InstallCommandTest extends KernelTestCase
|
|||
*/
|
||||
protected $application;
|
||||
|
||||
protected function getCommand(): Command
|
||||
protected function setUp(): void
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
|
@ -35,7 +34,11 @@ class InstallCommandTest extends KernelTestCase
|
|||
$container->getParameter('kernel.project_dir'),
|
||||
$container->get('doctrine')->getConnection()
|
||||
));
|
||||
}
|
||||
|
||||
return $this->application->find('kimai:install');
|
||||
public function testCommandName()
|
||||
{
|
||||
$command = $this->application->find('kimai:install');
|
||||
self::assertInstanceOf(InstallCommand::class, $command);
|
||||
}
|
||||
}
|
||||
|
|
60
tests/Controller/Reporting/ProjectViewControllerTest.php
Normal file
60
tests/Controller/Reporting/ProjectViewControllerTest.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\Tests\Controller\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Tests\Controller\ControllerBaseTest;
|
||||
use App\Tests\DataFixtures\ActivityFixtures;
|
||||
use App\Tests\DataFixtures\CustomerFixtures;
|
||||
use App\Tests\DataFixtures\ProjectFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
*/
|
||||
class ProjectViewControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testProjectViewIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/reporting/project_view');
|
||||
}
|
||||
|
||||
public function testProjectViewReport()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$customers = new CustomerFixtures();
|
||||
$customers->setIsVisible(true);
|
||||
$customers->setAmount(1);
|
||||
$customers = $this->importFixture($customers);
|
||||
|
||||
$projects = new ProjectFixtures();
|
||||
$projects->setCustomers($customers);
|
||||
$projects->setAmount(2);
|
||||
$projects->setIsVisible(true);
|
||||
$projects = $this->importFixture($projects);
|
||||
|
||||
$activities = new ActivityFixtures();
|
||||
$activities->setAmount(5);
|
||||
$activities->setIsGlobal(true);
|
||||
$activities = $this->importFixture($activities);
|
||||
|
||||
$timesheets = new TimesheetFixtures();
|
||||
$timesheets->setAmount(50);
|
||||
$timesheets->setActivities($activities);
|
||||
$timesheets->setUser($this->getUserByRole(User::ROLE_TEAMLEAD));
|
||||
$this->importFixture($timesheets);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/reporting/project_view');
|
||||
self::assertStringContainsString('<div class="box-body project-view-reporting-box', $client->getResponse()->getContent());
|
||||
$rows = $client->getCrawler()->filterXPath("//table[@id='dt_project_view_reporting']/tbody/tr");
|
||||
self::assertGreaterThan(0, $rows->count());
|
||||
}
|
||||
}
|
59
tests/Reporting/ProjectView/ProjectViewModelTest.php
Normal file
59
tests/Reporting/ProjectView/ProjectViewModelTest.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?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\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Reporting\ProjectView\ProjectViewModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Reporting\ProjectView\ProjectViewModel
|
||||
*/
|
||||
class ProjectViewModelTest extends TestCase
|
||||
{
|
||||
public function testDefaults()
|
||||
{
|
||||
$sut = new ProjectViewModel();
|
||||
|
||||
self::assertNull($sut->getProject());
|
||||
self::assertEquals(0, $sut->getDurationDay());
|
||||
self::assertEquals(0, $sut->getDurationMonth());
|
||||
self::assertEquals(0, $sut->getDurationTotal());
|
||||
self::assertEquals(0, $sut->getDurationWeek());
|
||||
self::assertEquals(0, $sut->getNotExportedDuration());
|
||||
self::assertEquals(0, $sut->getNotExportedRate());
|
||||
self::assertEquals(0, $sut->getRateTotal());
|
||||
}
|
||||
|
||||
public function testSetterGetter()
|
||||
{
|
||||
$sut = new ProjectViewModel();
|
||||
|
||||
$project = new Project();
|
||||
$sut->setProject($project);
|
||||
self::assertSame($project, $sut->getProject());
|
||||
|
||||
$sut->setDurationDay(123456789);
|
||||
$sut->setDurationMonth(23456789);
|
||||
$sut->setDurationTotal(3456789);
|
||||
$sut->setDurationWeek(456789);
|
||||
$sut->setNotExportedDuration(56789);
|
||||
$sut->setNotExportedRate(6789);
|
||||
$sut->setRateTotal(789);
|
||||
|
||||
self::assertEquals(123456789, $sut->getDurationDay());
|
||||
self::assertEquals(23456789, $sut->getDurationMonth());
|
||||
self::assertEquals(3456789, $sut->getDurationTotal());
|
||||
self::assertEquals(456789, $sut->getDurationWeek());
|
||||
self::assertEquals(56789, $sut->getNotExportedDuration());
|
||||
self::assertEquals(6789, $sut->getNotExportedRate());
|
||||
self::assertEquals(789, $sut->getRateTotal());
|
||||
}
|
||||
}
|
51
tests/Reporting/ProjectView/ProjectViewQueryTest.php
Normal file
51
tests/Reporting/ProjectView/ProjectViewQueryTest.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?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\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Reporting\ProjectView\ProjectViewQuery
|
||||
*/
|
||||
class ProjectViewQueryTest extends TestCase
|
||||
{
|
||||
public function testDefaults()
|
||||
{
|
||||
$user = new User();
|
||||
$date = new \DateTime();
|
||||
$sut = new ProjectViewQuery($date, $user);
|
||||
|
||||
self::assertSame($date, $sut->getToday());
|
||||
self::assertSame($user, $sut->getUser());
|
||||
self::assertNull($sut->getCustomer());
|
||||
self::assertFalse($sut->isIncludeNoBudget());
|
||||
self::assertFalse($sut->isIncludeNoWork());
|
||||
}
|
||||
|
||||
public function testSetterGetter()
|
||||
{
|
||||
$user = new User();
|
||||
$date = new \DateTime();
|
||||
$sut = new ProjectViewQuery($date, $user);
|
||||
|
||||
$customer = new Customer();
|
||||
|
||||
$sut->setCustomer($customer);
|
||||
$sut->setIncludeNoBudget(true);
|
||||
$sut->setIncludeNoWork(true);
|
||||
|
||||
self::assertSame($customer, $sut->getCustomer());
|
||||
self::assertTrue($sut->isIncludeNoBudget());
|
||||
self::assertTrue($sut->isIncludeNoWork());
|
||||
}
|
||||
}
|
|
@ -47,6 +47,6 @@ class ReportingServiceTest extends TestCase
|
|||
$sut = $this->getSut(true);
|
||||
$reports = $sut->getAvailableReports(new User());
|
||||
self::assertIsArray($reports);
|
||||
self::assertCount(3, $reports);
|
||||
self::assertCount(4, $reports);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,6 @@ class ReportingExtensionTest extends TestCase
|
|||
$sut = $this->getSut(true);
|
||||
$reports = $sut->getAvailableReports(new User());
|
||||
|
||||
$this->assertCount(3, $reports);
|
||||
$this->assertCount(4, $reports);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1130,6 +1130,14 @@
|
|||
<source>help.batch_meta_fields</source>
|
||||
<target>Felder die aktualisiert werden sollen, müssen zunächst durch einen Klick auf die zugehörige Checkbox freigeschaltet werden.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.includeNoWork">
|
||||
<source>label.includeNoWork</source>
|
||||
<target>Einträge ohne Buchungen anzeigen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.includeNoBudget">
|
||||
<source>label.includeNoBudget</source>
|
||||
<target>Einträge ohne Budget anzeigen</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1150,6 +1150,14 @@
|
|||
<source>help.batch_meta_fields</source>
|
||||
<target>Fields that are to be updated must first be activated by clicking on the associated checkbox.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.includeNoWork">
|
||||
<source>label.includeNoWork</source>
|
||||
<target>Show entries without bookings</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.includeNoBudget">
|
||||
<source>label.includeNoBudget</source>
|
||||
<target>Show entries without budget</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
<source>report_monthly_users</source>
|
||||
<target>Monatsansicht für alle Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_project_view">
|
||||
<source>report_project_view</source>
|
||||
<target>Projektübersicht</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
<source>report_monthly_users</source>
|
||||
<target>Monthly view for all users</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_project_view">
|
||||
<source>report_project_view</source>
|
||||
<target>Project overview</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
Loading…
Reference in a new issue