billable timesheets, inactive projects report, bookmark export search (#2503)
This commit is contained in:
@@ -304,7 +304,9 @@ final class KimaiImporterCommand extends Command
|
||||
$validationMessages = [];
|
||||
try {
|
||||
$usedEmails = [];
|
||||
$userIds = [];
|
||||
foreach ($users as $oldUser) {
|
||||
$userIds[] = $oldUser['userID'];
|
||||
if (empty($oldUser['mail'])) {
|
||||
$validationMessages[] = sprintf('User "%s" with ID %s has no email', $oldUser['name'], $oldUser['userID']);
|
||||
continue;
|
||||
@@ -325,6 +327,15 @@ final class KimaiImporterCommand extends Command
|
||||
$validationMessages[] = sprintf('Project "%s" with ID %s has unknown customer with ID %s', $oldProject['name'], $oldProject['projectID'], $oldProject['customerID']);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rates as $oldRate) {
|
||||
if ($oldRate['userID'] === null) {
|
||||
continue;
|
||||
}
|
||||
if (!\in_array($oldRate['userID'], $userIds)) {
|
||||
$validationMessages[] = sprintf('Unknown user with ID "%s" found for rate with project "%s" and activity "%s"', $oldRate['userID'], $oldRate['projectID'], $oldRate['activityID']);
|
||||
}
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$validationMessages[] = $ex->getMessage();
|
||||
}
|
||||
|
||||
@@ -196,7 +196,9 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
// apply bookmark ONLY if search form was not submitted manually
|
||||
if ($bookmark !== null && !$request->query->has('performSearch')) {
|
||||
$data->setBookmark($bookmark);
|
||||
$submitData = array_merge($bookmark->getContent(), $submitData);
|
||||
if (!$request->query->has('setDefaultQuery')) {
|
||||
$submitData = array_merge($bookmark->getContent(), $submitData);
|
||||
}
|
||||
}
|
||||
|
||||
// clean up parameters from unknown search values
|
||||
|
||||
@@ -311,7 +311,7 @@ final class ActivityController extends AbstractController
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.activityUpdate kimai.activityDelete',
|
||||
'data-form-event' => 'kimai.activityDelete',
|
||||
'data-msg-success' => 'action.delete.success',
|
||||
'data-msg-error' => 'action.delete.error',
|
||||
]
|
||||
|
||||
@@ -376,7 +376,7 @@ final class CustomerController extends AbstractController
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerUpdate kimai.customerDelete',
|
||||
'data-form-event' => 'kimai.customerDelete',
|
||||
'data-msg-success' => 'action.delete.success',
|
||||
'data-msg-error' => 'action.delete.error',
|
||||
]
|
||||
|
||||
@@ -15,7 +15,6 @@ use App\Form\Toolbar\ExportToolbarForm;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -50,26 +49,40 @@ class ExportController extends AbstractController
|
||||
$entries = [];
|
||||
|
||||
$form = $this->getToolbarForm($query, 'GET');
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('export');
|
||||
}
|
||||
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query);
|
||||
$byCustomer = [];
|
||||
|
||||
if ($form->isValid() && ($query->hasBookmark() || $request->query->has('performSearch'))) {
|
||||
$showPreview = true;
|
||||
$entries = $this->getEntries($query);
|
||||
foreach ($entries as $entry) {
|
||||
$cid = $entry->getProject()->getCustomer()->getId();
|
||||
if (!isset($byCustomer[$cid])) {
|
||||
$byCustomer[$cid] = [
|
||||
'customer' => $entry->getProject()->getCustomer(),
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
'duration' => 0,
|
||||
];
|
||||
}
|
||||
$byCustomer[$cid]['rate'] += $entry->getRate();
|
||||
$byCustomer[$cid]['internalRate'] += $entry->getInternalRate();
|
||||
$byCustomer[$cid]['duration'] += $entry->getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('export/index.html.twig', [
|
||||
'by_customer' => $byCustomer,
|
||||
'query' => $query,
|
||||
'entries' => $entries,
|
||||
'form' => $form->createView(),
|
||||
'renderer' => $this->export->getRenderer(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_limit' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
'decimal' => $this->getUser()->isExportDecimal(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ final class ProjectController extends AbstractController
|
||||
|
||||
$deleteForm = $this->createFormBuilder(null, [
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectUpdate kimai.projectDelete',
|
||||
'data-form-event' => 'kimai.projectDelete',
|
||||
'data-msg-success' => 'action.delete.success',
|
||||
'data-msg-error' => 'action.delete.error',
|
||||
]
|
||||
|
||||
57
src/Controller/Reporting/InactiveProjectController.php
Normal file
57
src/Controller/Reporting/InactiveProjectController.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\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveForm;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use App\Reporting\ProjectStatisticService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class InactiveProjectController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_inactive", name="report_project_inactive", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('budget_project')")
|
||||
*/
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectInactiveQuery($dateFactory->createDateTime('-1 year'), $user);
|
||||
$form = $this->createForm(ProjectInactiveForm::class, $query, [
|
||||
'timezone' => $user->getTimezone()
|
||||
]);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$projects = $service->findInactiveProjects($query);
|
||||
$entries = $service->getProjectView($user, $projects, $query->getLastChange());
|
||||
|
||||
$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(),
|
||||
'title' => 'report_inactive_project',
|
||||
'tableName' => 'inactive_project_reporting',
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Reporting\ProjectStatisticService;
|
||||
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;
|
||||
@@ -23,17 +23,17 @@ 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)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
{
|
||||
$query = new ProjectViewQuery($this->getDateTimeFactory()->createDateTime(), $this->getUser());
|
||||
|
||||
$form = $this->createForm(ProjectViewForm::class, $query, [
|
||||
'action' => $this->generateUrl('report_project_view')
|
||||
]);
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectViewQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createForm(ProjectViewForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$entries = $service->getProjectView($query);
|
||||
$projects = $service->findProjectsForView($query);
|
||||
$entries = $service->getProjectView($user, $projects, $query->getToday());
|
||||
|
||||
$byCustomer = [];
|
||||
foreach ($entries as $entry) {
|
||||
@@ -47,6 +47,9 @@ final class ProjectViewController extends AbstractController
|
||||
return $this->render('reporting/project_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'title' => 'report_project_view',
|
||||
'tableName' => 'project_view_reporting',
|
||||
'now' => $this->getDateTimeFactory()->createDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
public const TIMERANGE_RUNNING = 1047; // in minutes = 17:45 hours
|
||||
public const MIN_MINUTES_PER_ENTRY = 15;
|
||||
public const MAX_MINUTES_PER_ENTRY = 840; // 14h
|
||||
public const MAX_DESCRIPTION_LENGTH = 500;
|
||||
public const MAX_DESCRIPTION_LENGTH = 200;
|
||||
|
||||
public const ADD_TAGS_MAX_ENTRIES = 10000;
|
||||
public const MAX_TAG_PER_ENTRY = 3;
|
||||
|
||||
@@ -241,7 +241,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Entity"})
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="exported", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
@@ -250,6 +250,9 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
|
||||
@@ -380,6 +380,11 @@ class User extends BaseUser implements UserInterface
|
||||
return $this->getPreferenceValue('theme.layout', 'fixed') === 'boxed';
|
||||
}
|
||||
|
||||
public function isExportDecimal(): bool
|
||||
{
|
||||
return (bool) $this->getPreferenceValue('timesheet.export_decimal', false);
|
||||
}
|
||||
|
||||
public function setTimezone(?string $timezone)
|
||||
{
|
||||
if ($timezone === null) {
|
||||
|
||||
@@ -47,7 +47,17 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($event->isIndexView() && $this->isGranted('delete', $timesheet)) {
|
||||
$event->addAction('trash', ['url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-event' => 'kimai.timesheetDelete kimai.timesheetUpdate', 'data-method' => 'DELETE', 'data-question' => 'confirm.delete', 'data-msg-error' => 'action.delete.error', 'data-msg-success' => 'action.delete.success']]);
|
||||
$event->addAction('trash', [
|
||||
'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]),
|
||||
'class' => 'api-link',
|
||||
'attr' => [
|
||||
'data-event' => 'kimai.timesheetDelete',
|
||||
'data-method' => 'DELETE',
|
||||
'data-question' => 'confirm.delete',
|
||||
'data-msg-error' => 'action.delete.error',
|
||||
'data-msg-success' => 'action.delete.success'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
if ($activity->getId() === null) {
|
||||
return;
|
||||
}
|
||||
if ($this->isGranted('view', $activity)) {
|
||||
|
||||
if (!$event->isView('activity_details') && $this->isGranted('view', $activity)) {
|
||||
$event->addAction('details', ['url' => $this->path('activity_details', ['id' => $activity->getId()])]);
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
$event->addAction('create-timesheet', ['icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']);
|
||||
}
|
||||
|
||||
if ($event->isIndexView() && $this->isGranted('delete', $activity)) {
|
||||
if (($event->isIndexView() || $event->isView('project_details')) && $this->isGranted('delete', $activity)) {
|
||||
$event->addDelete($this->path('admin_activity_delete', ['id' => $activity->getId()]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class CustomerSubscriber extends AbstractActionsSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isGranted('view', $customer)) {
|
||||
if (!$event->isView('customer_details') && $this->isGranted('view', $customer)) {
|
||||
$event->addAction('details', ['url' => $this->path('customer_details', ['id' => $customer->getId()])]);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class CustomerSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($event->isIndexView() && $this->isGranted('delete', $customer)) {
|
||||
$event->addAction('trash', ['url' => $this->path('admin_customer_delete', ['id' => $customer->getId()]), 'class' => 'modal-ajax-form text-red']);
|
||||
$event->addDelete($this->path('admin_customer_delete', ['id' => $customer->getId()]));
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_reporting') && $this->isGranted('budget_project')) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isGranted('view', $project)) {
|
||||
if (!$event->isView('project_details') && $this->isGranted('view', $project)) {
|
||||
$event->addAction('details', ['url' => $this->path('project_details', ['id' => $project->getId()])]);
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
$event->addAction('copy', ['url' => $this->path('admin_project_duplicate', ['id' => $project->getId()])]);
|
||||
}
|
||||
|
||||
if ($event->isIndexView() && $this->isGranted('delete', $project)) {
|
||||
$event->addAction('trash', ['url' => $this->path('admin_project_delete', ['id' => $project->getId()]), 'class' => 'modal-ajax-form text-red']);
|
||||
if (($event->isIndexView() || $event->isView('customer_details')) && $this->isGranted('delete', $project)) {
|
||||
$event->addDelete($this->path('admin_project_delete', ['id' => $project->getId()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class UserSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($event->isIndexView() && $this->isGranted('delete', $user)) {
|
||||
$event->addAction('trash', ['url' => $this->path('admin_user_delete', ['id' => $user->getId()]), 'class' => 'modal-ajax-form']);
|
||||
$event->addDelete($this->path('admin_user_delete', ['id' => $user->getId()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
'wrapText' => false,
|
||||
],
|
||||
'exported' => [],
|
||||
'billable' => [],
|
||||
'tags' => [],
|
||||
'hourlyRate' => [],
|
||||
'fixedRate' => [],
|
||||
@@ -122,7 +123,16 @@ abstract class AbstractSpreadsheetRenderer
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$excelDate = Date::PHPToExcel($date);
|
||||
|
||||
if ($excelDate === false) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
|
||||
// TODO why is that format hardcoded and does not depend on the users locale?
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
|
||||
@@ -134,7 +144,15 @@ abstract class AbstractSpreadsheetRenderer
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$excelDate = Date::PHPToExcel($date);
|
||||
|
||||
if ($excelDate === false) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
|
||||
@@ -146,7 +164,16 @@ abstract class AbstractSpreadsheetRenderer
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$excelDate = Date::PHPToExcel($date);
|
||||
|
||||
if ($excelDate === false) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
|
||||
// TODO why is that format hardcoded and does not depend on the users locale?
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||
}
|
||||
|
||||
@@ -338,7 +365,14 @@ abstract class AbstractSpreadsheetRenderer
|
||||
|
||||
if (isset($columns['exported']) && !isset($columns['exported']['render'])) {
|
||||
$columns['exported']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) {
|
||||
$exported = $entity->isExported() ? 'entryState.exported' : 'entryState.not_exported';
|
||||
$exported = $entity->isExported() ? 'yes' : 'no';
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported));
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['billable']) && !isset($columns['billable']['render'])) {
|
||||
$columns['billable']['render'] = function (Worksheet $sheet, int $row, int $column, ExportItemInterface $entity) {
|
||||
$exported = (method_exists($entity, 'isBillable') && !$entity->isBillable()) ? 'no' : 'yes';
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,9 +71,9 @@ class HtmlRenderer
|
||||
{
|
||||
$decimal = false;
|
||||
if (null !== $query->getCurrentUser()) {
|
||||
$decimal = (bool) $query->getCurrentUser()->getPreferenceValue('timesheet.export_decimal', $decimal);
|
||||
$decimal = $query->getCurrentUser()->isExportDecimal();
|
||||
} elseif (null !== $query->getUser()) {
|
||||
$decimal = (bool) $query->getUser()->getPreferenceValue('timesheet.export_decimal', $decimal);
|
||||
$decimal = $query->getUser()->isExportDecimal();
|
||||
}
|
||||
|
||||
return ['decimal' => $decimal];
|
||||
|
||||
@@ -64,9 +64,9 @@ class PDFRenderer
|
||||
{
|
||||
$decimal = false;
|
||||
if (null !== $query->getCurrentUser()) {
|
||||
$decimal = (bool) $query->getCurrentUser()->getPreferenceValue('timesheet.export_decimal', $decimal);
|
||||
$decimal = $query->getCurrentUser()->isExportDecimal();
|
||||
} elseif (null !== $query->getUser()) {
|
||||
$decimal = (bool) $query->getUser()->getPreferenceValue('timesheet.export_decimal', $decimal);
|
||||
$decimal = $query->getUser()->isExportDecimal();
|
||||
}
|
||||
|
||||
return ['decimal' => $decimal];
|
||||
|
||||
@@ -21,6 +21,9 @@ interface ExportItemInterface extends InvoiceItemInterface
|
||||
*/
|
||||
public function isExported(): bool;
|
||||
|
||||
// will be activated with 2.0
|
||||
// public function isBillable(): bool;
|
||||
|
||||
/**
|
||||
* Returns the named meta field or null.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\Type\BillableType;
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use App\Form\Type\DescriptionType;
|
||||
use App\Form\Type\DurationType;
|
||||
@@ -128,6 +129,7 @@ class TimesheetEditForm extends AbstractType
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
|
||||
$this->addExported($builder, $options);
|
||||
$this->addBillable($builder, $options);
|
||||
}
|
||||
|
||||
protected function showCustomer(array $options, bool $isNew, int $customerCount): bool
|
||||
@@ -264,6 +266,15 @@ class TimesheetEditForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addBillable(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
if (!$options['include_billable']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$builder->add('billable', BillableType::class, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -276,6 +287,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'csrf_token_id' => 'timesheet_edit',
|
||||
'include_user' => false,
|
||||
'include_exported' => false,
|
||||
'include_billable' => true,
|
||||
'include_rate' => true,
|
||||
'docu_chapter' => 'timesheet.html',
|
||||
'method' => 'POST',
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace App\Form\Toolbar;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -27,6 +26,7 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addBillableChoice($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
if ($options['include_user']) {
|
||||
@@ -42,9 +42,6 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
'label' => 'label.mark_as_exported',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('preview', SubmitType::class, [
|
||||
'label' => 'button.preview',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addUsersChoice($builder);
|
||||
}
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addBillableChoice($builder);
|
||||
$this->addExportStateChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
|
||||
@@ -30,9 +30,7 @@ interface InvoiceItemInterface
|
||||
public function getRate(): float;
|
||||
|
||||
// will be activated with 2.0
|
||||
/*
|
||||
public function getInternalRate(): ?float;
|
||||
*/
|
||||
// public function getInternalRate(): ?float;
|
||||
|
||||
public function getUser(): ?User;
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ class Day
|
||||
* @var int
|
||||
*/
|
||||
protected $totalDuration = 0;
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $totalDurationBillable = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
@@ -54,6 +58,16 @@ class Day
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalDurationBillable(): int
|
||||
{
|
||||
return $this->totalDurationBillable;
|
||||
}
|
||||
|
||||
public function setTotalDurationBillable(int $seconds): void
|
||||
{
|
||||
$this->totalDurationBillable = $seconds;
|
||||
}
|
||||
|
||||
public function getTotalRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
|
||||
@@ -11,27 +11,14 @@ namespace App\Model\Statistic;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Monthly statistics
|
||||
*/
|
||||
class Month
|
||||
final class Month
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $month;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $totalDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $totalRate = 0.00;
|
||||
private $month;
|
||||
private $totalDuration = 0;
|
||||
private $totalRate = 0.00;
|
||||
private $billableDuration = 0;
|
||||
private $billableRate = 0.00;
|
||||
|
||||
/**
|
||||
* @param string $month
|
||||
*/
|
||||
public function __construct(string $month)
|
||||
{
|
||||
$monthNumber = (int) $month;
|
||||
@@ -43,14 +30,16 @@ class Month
|
||||
$this->month = $month;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMonth()
|
||||
public function getMonth(): string
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function getMonthNumber(): int
|
||||
{
|
||||
return (int) $this->month;
|
||||
}
|
||||
|
||||
public function getTotalDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
@@ -74,4 +63,24 @@ class Month
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillableDuration(): int
|
||||
{
|
||||
return $this->billableDuration;
|
||||
}
|
||||
|
||||
public function setBillableDuration(int $billableDuration): void
|
||||
{
|
||||
$this->billableDuration = $billableDuration;
|
||||
}
|
||||
|
||||
public function getBillableRate(): float
|
||||
{
|
||||
return $this->billableRate;
|
||||
}
|
||||
|
||||
public function setBillableRate(float $billableRate): void
|
||||
{
|
||||
$this->billableRate = $billableRate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,25 +23,19 @@ class Year
|
||||
*/
|
||||
protected $months = [];
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
*/
|
||||
public function __construct($year)
|
||||
public function __construct(string $year)
|
||||
{
|
||||
$this->year = $year;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getYear()
|
||||
public function getYear(): string
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function setMonth(Month $month): Year
|
||||
{
|
||||
$this->months[(int) $month->getMonth()] = $month;
|
||||
$this->months[$month->getMonthNumber()] = $month;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -11,22 +11,13 @@ namespace App\Model;
|
||||
|
||||
class TimesheetCountedStatistic
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $recordAmount = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $recordDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $recordRate = 0.0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $recordInternalRate = 0.0;
|
||||
private $recordAmount = 0;
|
||||
private $recordDuration = 0;
|
||||
private $recordRate = 0.0;
|
||||
private $recordInternalRate = 0.0;
|
||||
private $recordAmountBillable = 0;
|
||||
private $recordDurationBillable = 0;
|
||||
private $recordRateBillable = 0.0;
|
||||
|
||||
/**
|
||||
* Returns the total amount of included timesheet records.
|
||||
@@ -111,4 +102,34 @@ class TimesheetCountedStatistic
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRecordAmountBillable(): int
|
||||
{
|
||||
return $this->recordAmountBillable;
|
||||
}
|
||||
|
||||
public function setRecordAmountBillable(int $recordAmount): void
|
||||
{
|
||||
$this->recordAmountBillable = $recordAmount;
|
||||
}
|
||||
|
||||
public function getDurationBillable(): int
|
||||
{
|
||||
return $this->recordDurationBillable;
|
||||
}
|
||||
|
||||
public function setDurationBillable(int $recordDuration): void
|
||||
{
|
||||
$this->recordDurationBillable = $recordDuration;
|
||||
}
|
||||
|
||||
public function getRateBillable(): float
|
||||
{
|
||||
return $this->recordRateBillable;
|
||||
}
|
||||
|
||||
public function setRateBillable(float $recordRate): void
|
||||
{
|
||||
$this->recordRateBillable = $recordRate;
|
||||
}
|
||||
}
|
||||
|
||||
53
src/Reporting/ProjectInactive/ProjectInactiveForm.php
Normal file
53
src/Reporting/ProjectInactive/ProjectInactiveForm.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\ProjectInactive;
|
||||
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectInactiveForm 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('lastChange', DateTimePickerType::class, [
|
||||
'label' => 'label.last_record_before',
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectInactiveQuery::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
46
src/Reporting/ProjectInactive/ProjectInactiveQuery.php
Normal file
46
src/Reporting/ProjectInactive/ProjectInactiveQuery.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\ProjectInactive;
|
||||
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectInactiveQuery
|
||||
{
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $lastChange;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(DateTime $lastChange, User $user)
|
||||
{
|
||||
$this->lastChange = clone $lastChange;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getLastChange(): DateTime
|
||||
{
|
||||
return $this->lastChange;
|
||||
}
|
||||
|
||||
public function setLastChange(DateTime $lastChange): void
|
||||
{
|
||||
$this->lastChange = clone $lastChange;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,22 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting\ProjectView;
|
||||
namespace App\Reporting;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use App\Reporting\ProjectView\ProjectViewModel;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Exception;
|
||||
|
||||
final class ProjectViewService
|
||||
final class ProjectStatisticService
|
||||
{
|
||||
private $repository;
|
||||
private $timesheetRepository;
|
||||
@@ -28,28 +34,58 @@ final class ProjectViewService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return ProjectViewModel[]
|
||||
* @throws Exception
|
||||
* @param ProjectInactiveQuery $query
|
||||
* @return Project[]
|
||||
*/
|
||||
public function getProjectView(ProjectViewQuery $query): array
|
||||
public function findInactiveProjects(ProjectInactiveQuery $query): array
|
||||
{
|
||||
$factory = new DateTimeFactory($query->getToday()->getTimezone());
|
||||
$user = $query->getUser();
|
||||
$today = clone $query->getToday();
|
||||
$lastChange = clone $query->getLastChange();
|
||||
$now = new DateTime('now', $lastChange->getTimezone());
|
||||
|
||||
$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');
|
||||
$qb2 = $this->repository->createQueryBuilder('t1');
|
||||
$qb2
|
||||
->select('1')
|
||||
->from(Timesheet::class, 't')
|
||||
->andWhere('p = t.project')
|
||||
->andWhere($qb2->expr()->gte('t.begin', ':begin'))
|
||||
;
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p AS project')
|
||||
->addSelect('SUM(t.duration) AS totalDuration')
|
||||
->addSelect('SUM(t.rate) AS totalRate')
|
||||
->select('p, c')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->andWhere($qb->expr()->not($qb->expr()->exists($qb2)))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('p.end'),
|
||||
$qb->expr()->gte('p.end', ':project_end')
|
||||
)
|
||||
)
|
||||
->setParameter('project_end', $now, Types::DATETIME_MUTABLE)
|
||||
->setParameter('begin', $lastChange, Types::DATETIME_MUTABLE)
|
||||
;
|
||||
|
||||
$this->repository->addPermissionCriteria($qb, $user);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return Project[]
|
||||
*/
|
||||
public function findProjectsForView(ProjectViewQuery $query): array
|
||||
{
|
||||
$user = $query->getUser();
|
||||
$today = clone $query->getToday();
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p')
|
||||
->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))
|
||||
->andWhere(
|
||||
@@ -59,7 +95,6 @@ final class ProjectViewService
|
||||
)
|
||||
)
|
||||
->addGroupBy('p')
|
||||
->addGroupBy('t.project')
|
||||
->setParameter('project_end', $today, Types::DATETIME_MUTABLE)
|
||||
;
|
||||
|
||||
@@ -69,7 +104,10 @@ final class ProjectViewService
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoWork()) {
|
||||
$qb->andHaving($qb->expr()->gt('totalDuration', 0));
|
||||
$qb
|
||||
->leftJoin(Timesheet::class, 't', 'WITH', 'p.id = t.project')
|
||||
->andHaving($qb->expr()->gt('SUM(t.duration)', 0))
|
||||
;
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoBudget()) {
|
||||
@@ -83,23 +121,59 @@ final class ProjectViewService
|
||||
|
||||
$this->repository->addPermissionCriteria($qb, $user);
|
||||
|
||||
$result = $qb->getQuery()->getResult();
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Project[] $projects
|
||||
* @param DateTime|null $today
|
||||
* @return ProjectViewModel[]
|
||||
*/
|
||||
public function getProjectView(User $user, array $projects, ?DateTime $today = null): array
|
||||
{
|
||||
$factory = new DateTimeFactory(new DateTimeZone($user->getTimezone()));
|
||||
if (null === $today) {
|
||||
$today = $factory->createDateTime();
|
||||
}
|
||||
|
||||
$today = clone $today;
|
||||
|
||||
$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');
|
||||
|
||||
$projectViews = [];
|
||||
foreach ($result as $res) {
|
||||
$entity = new ProjectViewModel($res['project']);
|
||||
$entity->setDurationTotal($res['totalDuration'] ?? 0);
|
||||
$entity->setRateTotal($res['totalRate'] ?? 0.00);
|
||||
|
||||
$projectViews[$entity->getProject()->getId()] = $entity;
|
||||
foreach ($projects as $project) {
|
||||
$projectViews[$project->getId()] = new ProjectViewModel($project);
|
||||
}
|
||||
|
||||
$projectIds = array_keys($projectViews);
|
||||
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, COUNT(t.id) as amount, COALESCE(SUM(t.duration), 0) AS duration, COALESCE(SUM(t.rate), 0) AS rate, MAX(t.begin) as lastRecord')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->groupBy('t.project')
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationTotal($row['duration']);
|
||||
$projectViews[$row['id']]->setRateTotal($row['rate']);
|
||||
$projectViews[$row['id']]->setTimesheetCounter($row['amount']);
|
||||
if ($row['lastRecord'] !== null) {
|
||||
// might be the wrong timezone
|
||||
$projectViews[$row['id']]->setLastRecord($factory->createDateTime($row['lastRecord']));
|
||||
}
|
||||
}
|
||||
|
||||
// values for today
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) = :starting_date')
|
||||
->groupBy('t.project')
|
||||
@@ -109,13 +183,13 @@ final class ProjectViewService
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationDay($row['duration']);
|
||||
$projectViews[$row['id']]->setDurationDay($row['duration'] ?? 0);
|
||||
}
|
||||
|
||||
// values for the current week
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_date AND :end_date')
|
||||
->groupBy('t.project')
|
||||
@@ -132,7 +206,7 @@ final class ProjectViewService
|
||||
// values for the current month
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_month AND :end_month')
|
||||
->groupBy('t.project')
|
||||
@@ -146,10 +220,10 @@ final class ProjectViewService
|
||||
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
||||
}
|
||||
|
||||
// values for the all time (not exported)
|
||||
// values for 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')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration, COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.exported = :exported')
|
||||
->groupBy('t.project')
|
||||
@@ -163,10 +237,10 @@ final class ProjectViewService
|
||||
$projectViews[$row['id']]->setNotExportedRate($row['rate']);
|
||||
}
|
||||
|
||||
// values for the all time (not exported and billable)
|
||||
// values for all time (not exported and billable)
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration, SUM(t.rate) AS rate')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration, COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.exported = :exported')
|
||||
->andWhere('t.billable = :billable')
|
||||
@@ -182,6 +256,23 @@ final class ProjectViewService
|
||||
$projectViews[$row['id']]->setNotBilledRate($row['rate']);
|
||||
}
|
||||
|
||||
// values for all time (none billable)
|
||||
$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.billable = :billable')
|
||||
->groupBy('t.project')
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setBillableDuration($row['duration']);
|
||||
$projectViews[$row['id']]->setBillableRate($row['rate']);
|
||||
}
|
||||
|
||||
return array_values($projectViews);
|
||||
}
|
||||
}
|
||||
@@ -10,49 +10,24 @@
|
||||
namespace App\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Project;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectViewModel
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $timesheetCounter = 0;
|
||||
private $durationDay = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationWeek = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationMonth = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationTotal = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $rateTotal = 0.00;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $notExportedDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $notExportedRate = 0.00;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $notBilledDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $notBilledRate = 0.00;
|
||||
private $billableDuration = 0;
|
||||
private $billableRate = 0.00;
|
||||
private $lastRecord;
|
||||
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
@@ -64,6 +39,16 @@ final class ProjectViewModel
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function getTimesheetCounter(): int
|
||||
{
|
||||
return $this->timesheetCounter;
|
||||
}
|
||||
|
||||
public function setTimesheetCounter(int $timesheetCounter): void
|
||||
{
|
||||
$this->timesheetCounter = $timesheetCounter;
|
||||
}
|
||||
|
||||
public function getDurationDay(): int
|
||||
{
|
||||
return $this->durationDay;
|
||||
@@ -144,6 +129,26 @@ final class ProjectViewModel
|
||||
$this->notBilledRate = $notBilledRate;
|
||||
}
|
||||
|
||||
public function getBillableDuration(): int
|
||||
{
|
||||
return $this->billableDuration;
|
||||
}
|
||||
|
||||
public function setBillableDuration(int $billableDuration): void
|
||||
{
|
||||
$this->billableDuration = $billableDuration;
|
||||
}
|
||||
|
||||
public function getBillableRate(): float
|
||||
{
|
||||
return $this->billableRate;
|
||||
}
|
||||
|
||||
public function setBillableRate(float $billableRate): void
|
||||
{
|
||||
$this->billableRate = $billableRate;
|
||||
}
|
||||
|
||||
public function getRateTotal(): float
|
||||
{
|
||||
return $this->rateTotal;
|
||||
@@ -153,4 +158,14 @@ final class ProjectViewModel
|
||||
{
|
||||
$this->rateTotal = $rateTotal;
|
||||
}
|
||||
|
||||
public function getLastRecord(): ?DateTime
|
||||
{
|
||||
return $this->lastRecord;
|
||||
}
|
||||
|
||||
public function setLastRecord(DateTime $lastRecord): void
|
||||
{
|
||||
$this->lastRecord = $lastRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ final class ReportingService
|
||||
}
|
||||
if ($this->security->isGranted('budget_project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
|
||||
$event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project'));
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -97,9 +98,9 @@ class ActivityRepository extends EntityRepository
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internal_rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internal_rate')
|
||||
->where('t.activity = :activity')
|
||||
->setParameter('activity', $activity)
|
||||
;
|
||||
@@ -115,6 +116,26 @@ class ActivityRepository extends EntityRepository
|
||||
$stats->setRecordInternalRate($timesheetResult['internal_rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->where('t.activity = :activity')
|
||||
->andWhere('t.billable = :billable')
|
||||
->setParameter('activity', $activity)
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
$timesheetResult = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
if (null !== $timesheetResult) {
|
||||
$stats->setDurationBillable($timesheetResult['duration']);
|
||||
$stats->setRateBillable($timesheetResult['rate']);
|
||||
$stats->setRecordAmountBillable($timesheetResult['amount']);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -78,22 +79,16 @@ class CustomerRepository extends EntityRepository
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves statistics for one customer.
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @return CustomerStatistic
|
||||
*/
|
||||
public function getCustomerStatistics(Customer $customer)
|
||||
public function getCustomerStatistics(Customer $customer): CustomerStatistic
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internal_rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internal_rate')
|
||||
->andWhere('p.customer = :customer')
|
||||
->setParameter('customer', $customer)
|
||||
;
|
||||
@@ -109,6 +104,27 @@ class CustomerRepository extends EntityRepository
|
||||
$stats->setRecordInternalRate($timesheetResult['internal_rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->andWhere('p.customer = :customer')
|
||||
->andWhere('t.billable = :billable')
|
||||
->setParameter('customer', $customer)
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
$timesheetResult = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
if (null !== $timesheetResult) {
|
||||
$stats->setDurationBillable($timesheetResult['duration']);
|
||||
$stats->setRateBillable($timesheetResult['rate']);
|
||||
$stats->setRecordAmountBillable($timesheetResult['amount']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->select('COUNT(a.id) as amount')
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -84,18 +85,14 @@ class ProjectRepository extends EntityRepository
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.internalRate) as internal_rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internal_rate')
|
||||
->andWhere('t.project = :project')
|
||||
->setParameter('project', $project)
|
||||
;
|
||||
|
||||
if (null !== $begin) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':begin'))
|
||||
->setParameter('begin', $begin);
|
||||
}
|
||||
|
||||
// to calculate a budget at a certain point in time
|
||||
if (null !== $end) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':end'))
|
||||
->setParameter('end', $end);
|
||||
@@ -112,6 +109,32 @@ class ProjectRepository extends EntityRepository
|
||||
$stats->setRecordInternalRate($timesheetResult['internal_rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Timesheet::class, 't')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->andWhere('t.project = :project')
|
||||
->andWhere('t.billable = :billable')
|
||||
->setParameter('project', $project)
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
;
|
||||
|
||||
// to calculate a budget at a certain point in time
|
||||
if (null !== $end) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':end'))
|
||||
->setParameter('end', $end);
|
||||
}
|
||||
|
||||
$timesheetResult = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
if (null !== $timesheetResult) {
|
||||
$stats->setDurationBillable($timesheetResult['duration']);
|
||||
$stats->setRateBillable($timesheetResult['rate']);
|
||||
$stats->setRecordAmountBillable($timesheetResult['amount']);
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb
|
||||
->from(Activity::class, 'a')
|
||||
|
||||
@@ -304,6 +304,11 @@ class BaseQuery
|
||||
return $this->bookmark;
|
||||
}
|
||||
|
||||
public function hasBookmark(): bool
|
||||
{
|
||||
return null !== $this->bookmark;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
@@ -245,10 +245,10 @@ class TimesheetRepository extends EntityRepository
|
||||
return $this->getDailyStats($user, $begin, $end);
|
||||
|
||||
case self::STATS_QUERY_DURATION:
|
||||
$what = 'SUM(t.duration)';
|
||||
$what = 'COALESCE(SUM(t.duration), 0)';
|
||||
break;
|
||||
case self::STATS_QUERY_RATE:
|
||||
$what = 'SUM(t.rate)';
|
||||
$what = 'COALESCE(SUM(t.rate), 0)';
|
||||
break;
|
||||
case self::STATS_QUERY_USER:
|
||||
$what = 'COUNT(DISTINCT(t.user))';
|
||||
@@ -271,10 +271,16 @@ class TimesheetRepository extends EntityRepository
|
||||
*/
|
||||
protected function queryThisMonth($select, User $user)
|
||||
{
|
||||
$begin = new DateTime('first day of this month 00:00:00');
|
||||
$end = new DateTime('last day of this month 23:59:59');
|
||||
try {
|
||||
$timezone = new \DateTimeZone($user->getTimezone());
|
||||
$begin = new DateTime('first day of this month 00:00:00', $timezone);
|
||||
$end = new DateTime('last day of this month 23:59:59', $timezone);
|
||||
|
||||
return $this->queryTimeRange($select, $begin, $end, $user);
|
||||
return $this->queryTimeRange($select, $begin, $end, $user);
|
||||
} catch (\Exception $ex) {
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,20 +320,13 @@ class TimesheetRepository extends EntityRepository
|
||||
return empty($result) ? 0 : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch statistic data for one user.
|
||||
*
|
||||
* @param User $user
|
||||
* @return TimesheetStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getUserStatistics(User $user)
|
||||
public function getUserStatistics(User $user): TimesheetStatistic
|
||||
{
|
||||
$durationTotal = $this->getStatistic(self::STATS_QUERY_DURATION, null, null, $user);
|
||||
$recordsTotal = $this->getStatistic(self::STATS_QUERY_AMOUNT, null, null, $user);
|
||||
$rateTotal = $this->getStatistic(self::STATS_QUERY_RATE, null, null, $user);
|
||||
$amountMonth = $this->queryThisMonth('SUM(t.rate)', $user);
|
||||
$durationMonth = $this->queryThisMonth('SUM(t.duration)', $user);
|
||||
$amountMonth = $this->queryThisMonth('COALESCE(SUM(t.rate), 0)', $user);
|
||||
$durationMonth = $this->queryThisMonth('COALESCE(SUM(t.duration), 0)', $user);
|
||||
$firstEntry = $this->getEntityManager()
|
||||
->createQuery('SELECT MIN(t.begin) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
@@ -352,42 +351,15 @@ class TimesheetRepository extends EntityRepository
|
||||
* @param DateTime|null $end
|
||||
* @return Year[]
|
||||
*/
|
||||
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null)
|
||||
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('SUM(t.rate) as rate, SUM(t.duration) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year')
|
||||
->from(Timesheet::class, 't')
|
||||
;
|
||||
|
||||
if (!empty($begin)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':from'))
|
||||
->setParameter('from', $begin);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.begin'));
|
||||
}
|
||||
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':to'))
|
||||
->setParameter('to', $end);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
$qb
|
||||
->orderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->groupBy('year')
|
||||
->addGroupBy('month');
|
||||
|
||||
/** @var Year[] $years */
|
||||
$years = [];
|
||||
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, null);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
$curMonth = (int) $statRow['month'];
|
||||
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = new Year($curYear);
|
||||
@@ -398,15 +370,74 @@ class TimesheetRepository extends EntityRepository
|
||||
$years[$curYear] = $year;
|
||||
}
|
||||
|
||||
$month = new Month($statRow['month']);
|
||||
$month->setTotalDuration((int) $statRow['duration'])
|
||||
->setTotalRate((float) $statRow['rate']);
|
||||
$years[$curYear]->setMonth($month);
|
||||
$month = $years[$curYear]->getMonth($curMonth);
|
||||
$month->setTotalDuration((int) $statRow['duration']);
|
||||
$month->setTotalRate((float) $statRow['rate']);
|
||||
}
|
||||
|
||||
$qb = $this->getMonthlyStatsQuery($user, $begin, $end, true);
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
$curMonth = (int) $statRow['month'];
|
||||
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = new Year($curYear);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$month = $i < 10 ? '0' . $i : (string) $i;
|
||||
$year->setMonth(new Month($month));
|
||||
}
|
||||
$years[$curYear] = $year;
|
||||
}
|
||||
|
||||
$month = $years[$curYear]->getMonth($curMonth);
|
||||
$month->setBillableDuration((int) $statRow['duration']);
|
||||
$month->setBillableRate((float) $statRow['rate']);
|
||||
}
|
||||
|
||||
return $years;
|
||||
}
|
||||
|
||||
private function getMonthlyStatsQuery(User $user = null, ?DateTime $begin = null, ?DateTime $end = null, ?bool $billable = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->from(Timesheet::class, 't');
|
||||
$qb->select('COALESCE(SUM(t.rate), 0) as rate, COALESCE(SUM(t.duration), 0) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year');
|
||||
|
||||
if (!empty($begin)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.begin', ':from'));
|
||||
$qb->setParameter('from', $begin);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.begin'));
|
||||
}
|
||||
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.end', ':to'));
|
||||
$qb->setParameter('to', $end);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user');
|
||||
$qb->setParameter('user', $user);
|
||||
}
|
||||
|
||||
if (null !== $billable) {
|
||||
$qb->andWhere('t.billable = :billable');
|
||||
$qb->setParameter('billable', $billable);
|
||||
}
|
||||
|
||||
$qb
|
||||
->orderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
;
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* In case this method is called with one timezone and the results are from another timezone,
|
||||
* it might return rows outside the time-range.
|
||||
@@ -481,6 +512,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$results[$dateKey] = [
|
||||
'rate' => 0,
|
||||
'duration' => 0,
|
||||
'billable' => 0, // duration
|
||||
'month' => $beginTmp->format('n'),
|
||||
'year' => $beginTmp->format('Y'),
|
||||
'day' => $beginTmp->format('j'),
|
||||
@@ -496,6 +528,9 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
$results[$dateKey]['rate'] += $rate;
|
||||
$results[$dateKey]['duration'] += $duration;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['billable'] += $duration;
|
||||
}
|
||||
$detailsId =
|
||||
$result->getProject()->getCustomer()->getId()
|
||||
. '_' . $result->getProject()->getId()
|
||||
@@ -508,11 +543,15 @@ class TimesheetRepository extends EntityRepository
|
||||
'activity' => $result->getActivity(),
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'billable' => 0, // duration
|
||||
];
|
||||
}
|
||||
|
||||
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
|
||||
$results[$dateKey]['details'][$detailsId]['rate'] += $rate;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['details'][$detailsId]['billable'] += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
$beginTmp = $newDateBegin;
|
||||
@@ -562,6 +601,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
|
||||
$dateTime->setTime(0, 0, 0);
|
||||
$day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
|
||||
$day->setTotalDurationBillable($statRow['billable']);
|
||||
$day->setDetails($statRow['details']);
|
||||
$dateKey = $dateTime->format('Ymd');
|
||||
// make sure entries from other timezones are filtered
|
||||
|
||||
@@ -73,4 +73,26 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
return $event->getTranslations();
|
||||
}
|
||||
|
||||
public function getProgressbarClass(float $percent, ?bool $reverseColors = false): string
|
||||
{
|
||||
$colors = ['xl' => 'progress-bar-danger', 'l' => 'progress-bar-warning', 'm' => 'progress-bar-success', 's' => 'progress-bar-primary', 'e' => 'progress-bar-info'];
|
||||
if (true === $reverseColors) {
|
||||
$colors = ['s' => 'progress-bar-danger', 'm' => 'progress-bar-warning', 'l' => 'progress-bar-success', 'xl' => 'progress-bar-primary', 'e' => 'progress-bar-info'];
|
||||
}
|
||||
|
||||
if ($percent > 90) {
|
||||
$class = $colors['xl'];
|
||||
} elseif ($percent > 70) {
|
||||
$class = $colors['l'];
|
||||
} elseif ($percent > 50) {
|
||||
$class = $colors['m'];
|
||||
} elseif ($percent > 30) {
|
||||
$class = $colors['s'];
|
||||
} else {
|
||||
$class = $colors['e'];
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class RuntimeExtensions extends AbstractExtension
|
||||
return [
|
||||
new TwigFunction('trigger', [ThemeExtension::class, 'trigger'], ['needs_environment' => true]),
|
||||
new TwigFunction('actions', [ThemeExtension::class, 'actions']),
|
||||
new TwigFunction('progressbar_color', [ThemeExtension::class, 'getProgressbarClass']),
|
||||
new TwigFunction('javascript_translations', [ThemeExtension::class, 'getJavascriptTranslations']),
|
||||
new TwigFunction('active_timesheets', [TimesheetExtension::class, 'activeEntries']),
|
||||
new TwigFunction('encore_entry_css_source', [EncoreExtension::class, 'getEncoreEntryCssSource']),
|
||||
|
||||
Reference in New Issue
Block a user