added search modal for timesheet export (#2728)
This commit is contained in:
@@ -105,7 +105,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
|||||||
|
|
||||||
// load new form from given content
|
// load new form from given content
|
||||||
if (jQuery(html).find('#form_modal .modal-content').length > 0) {
|
if (jQuery(html).find('#form_modal .modal-content').length > 0) {
|
||||||
// switch classes, in case the modal type changed
|
// Support changing modal importance/types
|
||||||
remoteModal.on('hidden.bs.modal', function () {
|
remoteModal.on('hidden.bs.modal', function () {
|
||||||
if (remoteModal.hasClass('modal-danger')) {
|
if (remoteModal.hasClass('modal-danger')) {
|
||||||
remoteModal.removeClass('modal-danger');
|
remoteModal.removeClass('modal-danger');
|
||||||
@@ -116,6 +116,16 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
|||||||
remoteModal.addClass('modal-danger');
|
remoteModal.addClass('modal-danger');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support changing modal sizes
|
||||||
|
let modalDialog = remoteModal.find('.modal-dialog');
|
||||||
|
let largeModal = jQuery(html).find('.modal-dialog').hasClass('modal-lg');
|
||||||
|
if (largeModal && !modalDialog.hasClass('modal-lg')) {
|
||||||
|
modalDialog.addClass('modal-lg');
|
||||||
|
}
|
||||||
|
if (!largeModal && modalDialog.hasClass('modal-lg')) {
|
||||||
|
modalDialog.removeClass('modal-lg');
|
||||||
|
}
|
||||||
|
|
||||||
jQuery('#remote_form_modal .modal-content').replaceWith(
|
jQuery('#remote_form_modal .modal-content').replaceWith(
|
||||||
jQuery(html).find('#form_modal .modal-content')
|
jQuery(html).find('#form_modal .modal-content')
|
||||||
);
|
);
|
||||||
@@ -155,7 +165,13 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
|||||||
form.on('change', this._isDirtyHandler);
|
form.on('change', this._isDirtyHandler);
|
||||||
|
|
||||||
// click handler for modal save button, to send forms via ajax
|
// click handler for modal save button, to send forms via ajax
|
||||||
form.on('submit', function(event){
|
form.on('submit', function(event) {
|
||||||
|
// if the form has a target, we let the normal HTML flow happen
|
||||||
|
if (form.attr('target') !== undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise we do some AJAX magic to process the form in the background
|
||||||
const btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
|
const btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
|
||||||
const eventName = form.attr('data-form-event');
|
const eventName = form.attr('data-form-event');
|
||||||
const events = self.getContainer().getPlugin('event');
|
const events = self.getContainer().getPlugin('event');
|
||||||
|
|||||||
@@ -31,3 +31,4 @@
|
|||||||
@import 'progressbar';
|
@import 'progressbar';
|
||||||
@import 'avatar';
|
@import 'avatar';
|
||||||
@import 'security';
|
@import 'security';
|
||||||
|
@import 'tabs';
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
.modal-header,
|
||||||
|
.modal-footer {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 10px 80px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 10px 80px rgba(0, 0, 0, 0.6);
|
||||||
|
|||||||
3
assets/sass/tabs.scss
Normal file
3
assets/sass/tabs.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.tab-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/app.e22732af.js
Normal file
2
public/build/app.e22732af.js
Normal file
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"build/runtime.b8e7bb04.js",
|
"build/runtime.b8e7bb04.js",
|
||||||
"build/app.39dfa7e1.js"
|
"build/app.e22732af.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"build/app.068472ae.css"
|
"build/app.96253ac9.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"invoice": {
|
"invoice": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "build/app.068472ae.css",
|
"build/app.css": "build/app.96253ac9.css",
|
||||||
"build/app.js": "build/app.39dfa7e1.js",
|
"build/app.js": "build/app.e22732af.js",
|
||||||
"build/invoice.css": "build/invoice.ff32661a.css",
|
"build/invoice.css": "build/invoice.ff32661a.css",
|
||||||
"build/invoice.js": "build/invoice.19f36eca.js",
|
"build/invoice.js": "build/invoice.19f36eca.js",
|
||||||
"build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css",
|
"build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css",
|
||||||
|
|||||||
@@ -166,6 +166,31 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
|||||||
return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale);
|
return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getLastSearch(BaseQuery $query): ?array
|
||||||
|
{
|
||||||
|
$name = 'search_' . $this->getSearchName($query);
|
||||||
|
|
||||||
|
if (!$this->get('session')->has($name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->get('session')->get($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSearchName(BaseQuery $query): string
|
||||||
|
{
|
||||||
|
return substr($query->getName(), 0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected function ignorePersistedSearch(Request $request): void
|
||||||
|
{
|
||||||
|
$request->query->set('performSearch', true);
|
||||||
|
}
|
||||||
|
|
||||||
protected function handleSearch(FormInterface $form, Request $request): bool
|
protected function handleSearch(FormInterface $form, Request $request): bool
|
||||||
{
|
{
|
||||||
$data = $form->getData();
|
$data = $form->getData();
|
||||||
@@ -173,23 +198,35 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
|||||||
throw new \InvalidArgumentException('handleSearchForm() requires an instanceof BaseQuery as form data');
|
throw new \InvalidArgumentException('handleSearchForm() requires an instanceof BaseQuery as form data');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var BookmarkRepository $bookmarkRepo */
|
|
||||||
$bookmarkRepo = $this->getDoctrine()->getRepository(Bookmark::class);
|
|
||||||
$bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $data->getName());
|
|
||||||
|
|
||||||
$submitData = $request->query->all();
|
$submitData = $request->query->all();
|
||||||
|
// allow to use forms with block-prefix
|
||||||
// remove bookmark
|
if (!empty($formName = $form->getConfig()->getName()) && $request->request->has($formName)) {
|
||||||
if ($bookmark !== null && $request->query->has('removeDefaultQuery')) {
|
$submitData = $request->request->get($formName);
|
||||||
$bookmarkRepo->deleteBookmark($bookmark);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply bookmark ONLY if search form was not submitted manually
|
$searchName = $this->getSearchName($data);
|
||||||
if ($bookmark !== null && !$request->query->has('performSearch')) {
|
|
||||||
$data->setBookmark($bookmark);
|
/** @var BookmarkRepository $bookmarkRepo */
|
||||||
if (!$request->query->has('setDefaultQuery')) {
|
$bookmarkRepo = $this->getDoctrine()->getRepository(Bookmark::class);
|
||||||
|
$bookmark = $bookmarkRepo->getSearchDefaultOptions($this->getUser(), $searchName);
|
||||||
|
|
||||||
|
if ($bookmark !== null) {
|
||||||
|
if ($request->query->has('removeDefaultQuery')) {
|
||||||
|
$bookmarkRepo->deleteBookmark($bookmark);
|
||||||
|
$bookmark = null;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$data->setBookmark($bookmark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply persisted search data ONLY if search form was not submitted manually
|
||||||
|
if (!$request->query->has('performSearch')) {
|
||||||
|
$sessionSearch = $this->getLastSearch($data);
|
||||||
|
if ($sessionSearch !== null) {
|
||||||
|
$submitData = array_merge($sessionSearch, $submitData);
|
||||||
|
} elseif ($bookmark !== null && !$request->query->has('setDefaultQuery')) {
|
||||||
$submitData = array_merge($bookmark->getContent(), $submitData);
|
$submitData = array_merge($bookmark->getContent(), $submitData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,24 +242,39 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
|||||||
|
|
||||||
if (!$form->isValid()) {
|
if (!$form->isValid()) {
|
||||||
$data->resetByFormError($form->getErrors(true));
|
$data->resetByFormError($form->getErrors(true));
|
||||||
} elseif ($request->query->has('setDefaultQuery')) {
|
|
||||||
$params = [];
|
|
||||||
foreach ($form->all() as $name => $child) {
|
|
||||||
$params[$name] = $child->getViewData();
|
|
||||||
}
|
|
||||||
|
|
||||||
$filter = ['page', 'setDefaultQuery', 'removeDefaultQuery', 'performSearch'];
|
return false;
|
||||||
foreach ($filter as $name) {
|
}
|
||||||
if (isset($params[$name])) {
|
|
||||||
unset($params[$name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
foreach ($form->all() as $name => $child) {
|
||||||
|
$params[$name] = $child->getViewData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// these should NEVER be saved
|
||||||
|
$filter = ['setDefaultQuery', 'removeDefaultQuery', 'performSearch'];
|
||||||
|
foreach ($filter as $name) {
|
||||||
|
if (isset($params[$name])) {
|
||||||
|
unset($params[$name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->get('session')->set('search_' . $searchName, $params);
|
||||||
|
|
||||||
|
// filter stuff, that does not belong in a bookmark
|
||||||
|
$filter = ['page'];
|
||||||
|
foreach ($filter as $name) {
|
||||||
|
if (isset($params[$name])) {
|
||||||
|
unset($params[$name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('setDefaultQuery')) {
|
||||||
if ($bookmark === null) {
|
if ($bookmark === null) {
|
||||||
$bookmark = new Bookmark();
|
$bookmark = new Bookmark();
|
||||||
$bookmark->setType(Bookmark::SEARCH_DEFAULT);
|
$bookmark->setType(Bookmark::SEARCH_DEFAULT);
|
||||||
$bookmark->setUser($this->getUser());
|
$bookmark->setUser($this->getUser());
|
||||||
$bookmark->setName(substr($data->getName(), 0, 50));
|
$bookmark->setName($searchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookmark->setContent($params);
|
$bookmark->setContent($params);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use App\Form\MultiUpdate\MultiUpdateTableDTO;
|
|||||||
use App\Form\MultiUpdate\TimesheetMultiUpdate;
|
use App\Form\MultiUpdate\TimesheetMultiUpdate;
|
||||||
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
|
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
|
||||||
use App\Form\TimesheetEditForm;
|
use App\Form\TimesheetEditForm;
|
||||||
|
use App\Form\Toolbar\TimesheetExportToolbarForm;
|
||||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
@@ -33,6 +34,7 @@ use App\Timesheet\TimesheetService;
|
|||||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -47,10 +49,6 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
* @var EventDispatcherInterface
|
* @var EventDispatcherInterface
|
||||||
*/
|
*/
|
||||||
protected $dispatcher;
|
protected $dispatcher;
|
||||||
/**
|
|
||||||
* @var ServiceExport
|
|
||||||
*/
|
|
||||||
protected $exportService;
|
|
||||||
/**
|
/**
|
||||||
* @var TimesheetService
|
* @var TimesheetService
|
||||||
*/
|
*/
|
||||||
@@ -63,13 +61,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
TimesheetRepository $repository,
|
TimesheetRepository $repository,
|
||||||
EventDispatcherInterface $dispatcher,
|
EventDispatcherInterface $dispatcher,
|
||||||
ServiceExport $exportService,
|
|
||||||
TimesheetService $timesheetService,
|
TimesheetService $timesheetService,
|
||||||
SystemConfiguration $configuration
|
SystemConfiguration $configuration
|
||||||
) {
|
) {
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
$this->exportService = $exportService;
|
|
||||||
$this->service = $timesheetService;
|
$this->service = $timesheetService;
|
||||||
$this->configuration = $configuration;
|
$this->configuration = $configuration;
|
||||||
}
|
}
|
||||||
@@ -241,40 +237,53 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function export(Request $request, string $exporterId): Response
|
protected function export(Request $request, ServiceExport $serviceExport): Response
|
||||||
{
|
{
|
||||||
$query = new TimesheetQuery();
|
$query = $this->createDefaultQuery();
|
||||||
|
|
||||||
$form = $this->getToolbarForm($query);
|
$form = $this->getExportForm($query);
|
||||||
$form->setData($query);
|
|
||||||
$form->submit($request->query->all(), false);
|
|
||||||
|
|
||||||
$factory = $this->getDateTimeFactory();
|
if ($request->isMethod(Request::METHOD_POST)) {
|
||||||
|
$this->ignorePersistedSearch($request);
|
||||||
// by default the current month is exported, but it can be overwritten
|
|
||||||
// this should not be removed, otherwise we would export EVERY available record in the admin section
|
|
||||||
// as the default toolbar query does neither limit the user nor the date-range!
|
|
||||||
if (null === $query->getBegin()) {
|
|
||||||
$query->setBegin($factory->getStartOfMonth());
|
|
||||||
}
|
}
|
||||||
$query->getBegin()->setTime(0, 0, 0);
|
|
||||||
|
|
||||||
if (null === $query->getEnd()) {
|
if ($this->handleSearch($form, $request)) {
|
||||||
$query->setEnd($factory->getEndOfMonth());
|
return $this->redirectToRoute($this->getExportRoute());
|
||||||
}
|
}
|
||||||
$query->getEnd()->setTime(23, 59, 59);
|
|
||||||
|
|
||||||
$this->prepareQuery($query);
|
$this->prepareQuery($query);
|
||||||
|
|
||||||
$entries = $this->repository->getTimesheetsForQuery($query);
|
// make sure that we use the "expected time range"
|
||||||
|
if (null !== $query->getBegin()) {
|
||||||
$exporter = $this->exportService->getTimesheetExporterById($exporterId);
|
$query->getBegin()->setTime(0, 0, 0);
|
||||||
|
}
|
||||||
if (null === $exporter) {
|
if (null !== $query->getEnd()) {
|
||||||
throw $this->createNotFoundException('Invalid timesheet exporter given');
|
$query->getEnd()->setTime(23, 59, 59);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $exporter->render($entries, $query);
|
$entries = $this->repository->getTimesheetResult($query);
|
||||||
|
$stats = $entries->getStatistic();
|
||||||
|
|
||||||
|
// perform the real export
|
||||||
|
if ($request->isMethod(Request::METHOD_POST)) {
|
||||||
|
$type = $request->request->get('exporter');
|
||||||
|
if (null !== $type) {
|
||||||
|
$exporter = $serviceExport->getTimesheetExporterById($type);
|
||||||
|
|
||||||
|
if (null === $exporter) {
|
||||||
|
$form->addError(new FormError('Invalid timesheet exporter given'));
|
||||||
|
} else {
|
||||||
|
return $exporter->render($entries->getResults(true), $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('timesheet/layout-export.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'route_back' => $this->getTimesheetRoute(),
|
||||||
|
'exporter' => $serviceExport->getTimesheetExporter(),
|
||||||
|
'stats' => $stats,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function multiUpdate(Request $request, string $renderTemplate)
|
protected function multiUpdate(Request $request, string $renderTemplate)
|
||||||
@@ -518,6 +527,16 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getExportForm(TimesheetQuery $query): FormInterface
|
||||||
|
{
|
||||||
|
return $this->createForm(TimesheetExportToolbarForm::class, $query, [
|
||||||
|
'action' => $this->generateUrl($this->getExportRoute()),
|
||||||
|
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||||
|
'method' => Request::METHOD_POST,
|
||||||
|
'include_user' => $this->includeUserInForms('toolbar'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getPermissionEditExport(): string
|
protected function getPermissionEditExport(): string
|
||||||
{
|
{
|
||||||
return 'edit_export_own_timesheet';
|
return 'edit_export_own_timesheet';
|
||||||
@@ -563,11 +582,29 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
return 'timesheet_multi_delete';
|
return 'timesheet_multi_delete';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getExportRoute(): string
|
||||||
|
{
|
||||||
|
return 'timesheet_export';
|
||||||
|
}
|
||||||
|
|
||||||
protected function canSeeStartEndTime(): bool
|
protected function canSeeStartEndTime(): bool
|
||||||
{
|
{
|
||||||
return $this->getTrackingMode()->canSeeBeginAndEndTimes();
|
return $this->getTrackingMode()->canSeeBeginAndEndTimes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryNamePrefix(): string
|
||||||
|
{
|
||||||
|
return 'MyTimes';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDefaultQuery(string $suffix = 'Listing'): TimesheetQuery
|
||||||
|
{
|
||||||
|
$query = new TimesheetQuery();
|
||||||
|
$query->setName($this->getQueryNamePrefix() . $suffix);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
abstract protected function getDuplicateForm(Timesheet $entry): FormInterface;
|
abstract protected function getDuplicateForm(Timesheet $entry): FormInterface;
|
||||||
|
|
||||||
abstract protected function getCreateForm(Timesheet $entry): FormInterface;
|
abstract protected function getCreateForm(Timesheet $entry): FormInterface;
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
|
use App\Export\ServiceExport;
|
||||||
use App\Form\TimesheetEditForm;
|
use App\Form\TimesheetEditForm;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
|
||||||
use App\Repository\TagRepository;
|
use App\Repository\TagRepository;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
@@ -35,20 +35,19 @@ class TimesheetController extends TimesheetAbstractController
|
|||||||
*/
|
*/
|
||||||
public function indexAction(int $page, Request $request): Response
|
public function indexAction(int $page, Request $request): Response
|
||||||
{
|
{
|
||||||
$query = new TimesheetQuery();
|
$query = $this->createDefaultQuery();
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
$query->setName('MyTimesListing');
|
|
||||||
|
|
||||||
return $this->index($query, $request, 'timesheet', 'timesheet/index.html.twig', TimesheetMetaDisplayEvent::TIMESHEET);
|
return $this->index($query, $request, 'timesheet', 'timesheet/index.html.twig', TimesheetMetaDisplayEvent::TIMESHEET);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/export/{exporter}", name="timesheet_export", methods={"GET"})
|
* @Route(path="/export/", name="timesheet_export", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('export_own_timesheet')")
|
* @Security("is_granted('export_own_timesheet')")
|
||||||
*/
|
*/
|
||||||
public function exportAction(Request $request, string $exporter): Response
|
public function exportAction(Request $request, ServiceExport $serviceExport): Response
|
||||||
{
|
{
|
||||||
return $this->export($request, $exporter);
|
return $this->export($request, $serviceExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Entity\Team;
|
|||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
|
use App\Export\ServiceExport;
|
||||||
use App\Form\Model\MultiUserTimesheet;
|
use App\Form\Model\MultiUserTimesheet;
|
||||||
use App\Form\TimesheetAdminEditForm;
|
use App\Form\TimesheetAdminEditForm;
|
||||||
use App\Form\TimesheetMultiUserEditForm;
|
use App\Form\TimesheetMultiUserEditForm;
|
||||||
@@ -45,19 +46,19 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
*/
|
*/
|
||||||
public function indexAction(int $page, Request $request): Response
|
public function indexAction(int $page, Request $request): Response
|
||||||
{
|
{
|
||||||
$query = new TimesheetQuery();
|
$query = $this->createDefaultQuery();
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
$query->setName('TeamTimesListing');
|
|
||||||
|
|
||||||
return $this->index($query, $request, 'admin_timesheet', 'timesheet-team/index.html.twig', TimesheetMetaDisplayEvent::TEAM_TIMESHEET);
|
return $this->index($query, $request, 'admin_timesheet', 'timesheet-team/index.html.twig', TimesheetMetaDisplayEvent::TEAM_TIMESHEET);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/export/{exporter}", name="admin_timesheet_export", methods={"GET"})
|
* @Route(path="/export/", name="admin_timesheet_export", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('export_other_timesheet')")
|
||||||
*/
|
*/
|
||||||
public function exportAction(Request $request, string $exporter): Response
|
public function exportAction(Request $request, ServiceExport $serviceExport): Response
|
||||||
{
|
{
|
||||||
return $this->export($request, $exporter);
|
return $this->export($request, $serviceExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,6 +234,11 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
return 'admin_timesheet_edit';
|
return 'admin_timesheet_edit';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getExportRoute(): string
|
||||||
|
{
|
||||||
|
return 'admin_timesheet_export';
|
||||||
|
}
|
||||||
|
|
||||||
protected function getMultiUpdateRoute(): string
|
protected function getMultiUpdateRoute(): string
|
||||||
{
|
{
|
||||||
return 'admin_timesheet_multi_update';
|
return 'admin_timesheet_multi_update';
|
||||||
@@ -247,4 +253,9 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryNamePrefix(): string
|
||||||
|
{
|
||||||
|
return 'TeamTimes';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<?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\EventSubscriber\Actions;
|
|
||||||
|
|
||||||
use App\Event\PageActionsEvent;
|
|
||||||
use App\Export\ServiceExport;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
|
||||||
|
|
||||||
abstract class AbstractTimesheetsSubscriber extends AbstractActionsSubscriber
|
|
||||||
{
|
|
||||||
private $serviceExport;
|
|
||||||
|
|
||||||
public function __construct(AuthorizationCheckerInterface $security, UrlGeneratorInterface $urlGenerator, ServiceExport $serviceExport)
|
|
||||||
{
|
|
||||||
parent::__construct($security, $urlGenerator);
|
|
||||||
$this->serviceExport = $serviceExport;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function addExporter(PageActionsEvent $event, string $routeExport): void
|
|
||||||
{
|
|
||||||
$allExporter = $this->serviceExport->getTimesheetExporter();
|
|
||||||
if (\count($allExporter) === 1) {
|
|
||||||
$event->addAction('download', ['url' => $this->path($routeExport, ['exporter' => $allExporter[0]->getId()]), 'class' => 'toolbar-action']);
|
|
||||||
} else {
|
|
||||||
foreach ($allExporter as $exporter) {
|
|
||||||
$id = $exporter->getId();
|
|
||||||
$event->addActionToSubmenu('download', 'exporter.' . $id, ['title' => 'button.' . $id, 'url' => $this->path($routeExport, ['exporter' => $id]), 'class' => 'toolbar-action exporter-' . $id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ class ReportingSubscriber extends AbstractActionsSubscriber
|
|||||||
if ($report instanceof Report) {
|
if ($report instanceof Report) {
|
||||||
$subMenu = $report->getReportIcon();
|
$subMenu = $report->getReportIcon();
|
||||||
}
|
}
|
||||||
$event->addActionToSubmenu($subMenu, $report->getId(), ['title' => $report->getLabel(), 'translation_domain' => 'reporting', 'url' => $this->path($report->getRoute()), 'class' => 'toolbar-action report-' . $report->getId()]);
|
$event->addActionToSubmenu($subMenu, $report->getId(), ['title' => $report->getLabel(), 'translation_domain' => 'reporting', 'url' => $this->path($report->getRoute()), 'class' => 'report-' . $report->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$event->addHelp($this->documentationLink('reporting.html'));
|
$event->addHelp($this->documentationLink('reporting.html'));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace App\EventSubscriber\Actions;
|
|||||||
|
|
||||||
use App\Event\PageActionsEvent;
|
use App\Event\PageActionsEvent;
|
||||||
|
|
||||||
class TimesheetsSubscriber extends AbstractTimesheetsSubscriber
|
class TimesheetsSubscriber extends AbstractActionsSubscriber
|
||||||
{
|
{
|
||||||
public static function getActionName(): string
|
public static function getActionName(): string
|
||||||
{
|
{
|
||||||
@@ -24,7 +24,7 @@ class TimesheetsSubscriber extends AbstractTimesheetsSubscriber
|
|||||||
$event->addColumnToggle('#modal_timesheet');
|
$event->addColumnToggle('#modal_timesheet');
|
||||||
|
|
||||||
if ($this->isGranted('export_own_timesheet')) {
|
if ($this->isGranted('export_own_timesheet')) {
|
||||||
$this->addExporter($event, 'timesheet_export');
|
$event->addAction('download', ['url' => $this->path('timesheet_export'), 'class' => 'toolbar-action modal-ajax-form']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGranted('create_own_timesheet')) {
|
if ($this->isGranted('create_own_timesheet')) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace App\EventSubscriber\Actions;
|
|||||||
|
|
||||||
use App\Event\PageActionsEvent;
|
use App\Event\PageActionsEvent;
|
||||||
|
|
||||||
class TimesheetsTeamSubscriber extends AbstractTimesheetsSubscriber
|
class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
|
||||||
{
|
{
|
||||||
public static function getActionName(): string
|
public static function getActionName(): string
|
||||||
{
|
{
|
||||||
@@ -24,7 +24,7 @@ class TimesheetsTeamSubscriber extends AbstractTimesheetsSubscriber
|
|||||||
$event->addColumnToggle('#modal_timesheet_admin');
|
$event->addColumnToggle('#modal_timesheet_admin');
|
||||||
|
|
||||||
if ($this->isGranted('export_other_timesheet')) {
|
if ($this->isGranted('export_other_timesheet')) {
|
||||||
$this->addExporter($event, 'admin_timesheet_export');
|
$event->addAction('download', ['url' => $this->path('admin_timesheet_export'), 'class' => 'toolbar-action modal-ajax-form']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGranted('create_other_timesheet')) {
|
if ($this->isGranted('create_other_timesheet')) {
|
||||||
|
|||||||
@@ -706,36 +706,43 @@ abstract class AbstractSpreadsheetRenderer
|
|||||||
$entryHeaderRow++;
|
$entryHeaderRow++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $durationColumn) {
|
if ($this->isTotalRowSupported()) {
|
||||||
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
|
if (null !== $durationColumn) {
|
||||||
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
|
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
|
||||||
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||||
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
|
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
|
||||||
$style->getFont()->setBold(true);
|
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
}
|
$style->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (null !== $rateColumn) {
|
if (null !== $rateColumn) {
|
||||||
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
|
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
|
||||||
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
|
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||||
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||||
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
|
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
|
||||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
$style->getFont()->setBold(true);
|
$style->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $internalRateColumn) {
|
if (null !== $internalRateColumn) {
|
||||||
$startCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, 2)->getCoordinate();
|
$startCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, 2)->getCoordinate();
|
||||||
$endCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, $entryHeaderRow - 1)->getCoordinate();
|
$endCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||||
$this->setRateTotal($sheet, $internalRateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
$this->setRateTotal($sheet, $internalRateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||||
$style = $sheet->getStyleByColumnAndRow($internalRateColumn, $entryHeaderRow);
|
$style = $sheet->getStyleByColumnAndRow($internalRateColumn, $entryHeaderRow);
|
||||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||||
$style->getFont()->setBold(true);
|
$style->getFont()->setBold(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $spreadsheet;
|
return $spreadsheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function isTotalRowSupported(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ExportItemInterface[] $exportItems
|
* @param ExportItemInterface[] $exportItems
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
|
|||||||
@@ -167,7 +167,9 @@ trait RendererTrait
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$allBudgets = $projectStatisticService->getBudgetStatisticModelForProjects($projects, $query->getEnd());
|
$today = $this->getToday($query);
|
||||||
|
|
||||||
|
$allBudgets = $projectStatisticService->getBudgetStatisticModelForProjects($projects, $today);
|
||||||
|
|
||||||
foreach ($allBudgets as $projectId => $statisticModel) {
|
foreach ($allBudgets as $projectId => $statisticModel) {
|
||||||
$project = $statisticModel->getProject();
|
$project = $statisticModel->getProject();
|
||||||
@@ -183,6 +185,29 @@ trait RendererTrait
|
|||||||
return $summary;
|
return $summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getToday(TimesheetQuery $query): \DateTime
|
||||||
|
{
|
||||||
|
$end = $query->getEnd();
|
||||||
|
|
||||||
|
if ($end !== null) {
|
||||||
|
return $end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->getCurrentUser() !== null) {
|
||||||
|
$timezone = $query->getCurrentUser()->getTimezone();
|
||||||
|
|
||||||
|
return new \DateTime('now', new \DateTimeZone($timezone));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->getUser() !== null) {
|
||||||
|
$timezone = $query->getUser()->getTimezone();
|
||||||
|
|
||||||
|
return new \DateTime('now', new \DateTimeZone($timezone));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ExportItemInterface[] $exportItems
|
* @param ExportItemInterface[] $exportItems
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
@@ -236,7 +261,9 @@ trait RendererTrait
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$allBudgets = $activityStatisticService->getBudgetStatisticModelForActivities($activities, $query->getEnd());
|
$today = $this->getToday($query);
|
||||||
|
|
||||||
|
$allBudgets = $activityStatisticService->getBudgetStatisticModelForActivities($activities, $today);
|
||||||
|
|
||||||
foreach ($allBudgets as $activityId => $statisticModel) {
|
foreach ($allBudgets as $activityId => $statisticModel) {
|
||||||
$project = $statisticModel->getActivity()->getProject();
|
$project = $statisticModel->getActivity()->getProject();
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
|||||||
|
|
||||||
class XlsxRenderer extends AbstractSpreadsheetRenderer
|
class XlsxRenderer extends AbstractSpreadsheetRenderer
|
||||||
{
|
{
|
||||||
|
protected function isTotalRowSupported(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function getFileExtension(): string
|
public function getFileExtension(): string
|
||||||
{
|
{
|
||||||
return '.xlsx';
|
return '.xlsx';
|
||||||
|
|||||||
63
src/Form/Toolbar/TimesheetExportToolbarForm.php
Normal file
63
src/Form/Toolbar/TimesheetExportToolbarForm.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form\Toolbar;
|
||||||
|
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the form used for filtering the timesheet.
|
||||||
|
*/
|
||||||
|
class TimesheetExportToolbarForm extends AbstractToolbarForm
|
||||||
|
{
|
||||||
|
public function getBlockPrefix()
|
||||||
|
{
|
||||||
|
return 'export';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$newOptions = [];
|
||||||
|
if ($options['ignore_date'] === true) {
|
||||||
|
$newOptions['ignore_date'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addSearchTermInputField($builder);
|
||||||
|
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||||
|
$this->addCustomerMultiChoice($builder, $newOptions, true);
|
||||||
|
$this->addProjectMultiChoice($builder, $newOptions, true, true);
|
||||||
|
$this->addActivityMultiChoice($builder, [], true);
|
||||||
|
$this->addTagInputField($builder);
|
||||||
|
if ($options['include_user']) {
|
||||||
|
$this->addUsersChoice($builder);
|
||||||
|
}
|
||||||
|
$this->addTimesheetStateChoice($builder);
|
||||||
|
$this->addBillableChoice($builder);
|
||||||
|
$this->addExportStateChoice($builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => TimesheetQuery::class,
|
||||||
|
'csrf_protection' => false,
|
||||||
|
'include_user' => false,
|
||||||
|
'ignore_date' => true,
|
||||||
|
'timezone' => date_default_timezone_get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/Repository/Result/TimesheetResult.php
Normal file
56
src/Repository/Result/TimesheetResult.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Repository\Result;
|
||||||
|
|
||||||
|
use App\Repository\Loader\TimesheetLoader;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
class TimesheetResult
|
||||||
|
{
|
||||||
|
private $queryBuilder;
|
||||||
|
|
||||||
|
public function __construct(QueryBuilder $queryBuilder)
|
||||||
|
{
|
||||||
|
$this->queryBuilder = $queryBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatistic(): TimesheetResultStatistic
|
||||||
|
{
|
||||||
|
$qb = clone $this->queryBuilder;
|
||||||
|
$qb
|
||||||
|
->resetDQLPart('select')
|
||||||
|
->resetDQLPart('orderBy')
|
||||||
|
->select('COUNT(t.id) as counter')
|
||||||
|
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||||
|
;
|
||||||
|
|
||||||
|
$result = $qb->getQuery()->getArrayResult()[0];
|
||||||
|
|
||||||
|
return new TimesheetResultStatistic($result['counter'], $result['duration']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toIterable(): iterable
|
||||||
|
{
|
||||||
|
$query = $this->queryBuilder->getQuery();
|
||||||
|
|
||||||
|
return $query->toIterable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResults(bool $fullyHydrated = false): array
|
||||||
|
{
|
||||||
|
$query = $this->queryBuilder->getQuery();
|
||||||
|
$results = $query->getResult();
|
||||||
|
|
||||||
|
$loader = new TimesheetLoader($this->queryBuilder->getEntityManager(), $fullyHydrated);
|
||||||
|
$loader->loadResults($results);
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/Repository/Result/TimesheetResultStatistic.php
Normal file
32
src/Repository/Result/TimesheetResultStatistic.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Repository\Result;
|
||||||
|
|
||||||
|
class TimesheetResultStatistic
|
||||||
|
{
|
||||||
|
private $count = 0;
|
||||||
|
private $duration = 0;
|
||||||
|
|
||||||
|
public function __construct(int $count, int $duration)
|
||||||
|
{
|
||||||
|
$this->count = $count;
|
||||||
|
$this->duration = $duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCount(): int
|
||||||
|
{
|
||||||
|
return $this->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDuration(): int
|
||||||
|
{
|
||||||
|
return $this->duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ use App\Repository\Loader\TimesheetLoader;
|
|||||||
use App\Repository\Paginator\LoaderPaginator;
|
use App\Repository\Paginator\LoaderPaginator;
|
||||||
use App\Repository\Paginator\PaginatorInterface;
|
use App\Repository\Paginator\PaginatorInterface;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use App\Repository\Result\TimesheetResult;
|
||||||
use DateInterval;
|
use DateInterval;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
@@ -749,7 +750,7 @@ class TimesheetRepository extends EntityRepository
|
|||||||
* Especially the following question is still un-answered!
|
* Especially the following question is still un-answered!
|
||||||
*
|
*
|
||||||
* Should a teamlead:
|
* Should a teamlead:
|
||||||
* 1 . see all records of his team-members, even if they recorded times for projects invisible to him
|
* 1. see all records of his team-members, even if they recorded times for projects invisible to him
|
||||||
* 2. only see records for projects which can be accessed by hom (current situation)
|
* 2. only see records for projects which can be accessed by hom (current situation)
|
||||||
*/
|
*/
|
||||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): bool
|
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): bool
|
||||||
@@ -835,6 +836,13 @@ class TimesheetRepository extends EntityRepository
|
|||||||
return $this->getHydratedResultsByQuery($qb, $fullyHydrated);
|
return $this->getHydratedResultsByQuery($qb, $fullyHydrated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTimesheetResult(TimesheetQuery $query): TimesheetResult
|
||||||
|
{
|
||||||
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
|
||||||
|
return new TimesheetResult($qb);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param QueryBuilder $qb
|
* @param QueryBuilder $qb
|
||||||
* @param bool $fullyHydrated
|
* @param bool $fullyHydrated
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
data-title="{{- get_title()|e('html_attr') -}}"
|
data-title="{{- get_title()|e('html_attr') -}}"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_title %}{{- get_title() -}}{% endblock %}
|
||||||
|
|
||||||
{% block after_body_start %}
|
{% block after_body_start %}
|
||||||
{% embed 'embeds/modal.html.twig' %}
|
{% embed 'embeds/modal.html.twig' %}
|
||||||
{% block modal_id %}remote_form_modal{% endblock %}
|
{% block modal_id %}remote_form_modal{% endblock %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% set _reset = reset is defined and reset is same as (false) ? false : true %}
|
{% set _reset = reset is defined and reset is same as (false) ? false : true %}
|
||||||
<div class="box box-primary">
|
<div class="box box-primary">
|
||||||
{% block form_before %}{% endblock %}
|
{% block form_before %}{% endblock %}
|
||||||
{{ form_start(form) }}
|
{{ form_start(form, formStartOptions|default({})) }}
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border">
|
||||||
<h3 class="box-title">
|
<h3 class="box-title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
|
{% block submit_button %}
|
||||||
|
<input type="submit" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" value="{{ (submit_button|default('action.save'))|trans }}" class="btn btn-primary" />
|
||||||
|
{% endblock %}
|
||||||
{% if _back is not same as (false) %}
|
{% if _back is not same as (false) %}
|
||||||
<a href="{{ _back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
<a href="{{ _back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ REQUIRED CHANGES NEED TO BE DONE IN THESE FILES:
|
|||||||
- default/_form_modal.html.twig
|
- default/_form_modal.html.twig
|
||||||
#}
|
#}
|
||||||
<div class="modal {% block modal_class %}{% endblock %}" id="{% block modal_id %}form_modal{% endblock %}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
|
<div class="modal {% block modal_class %}{% endblock %}" id="{% block modal_id %}form_modal{% endblock %}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog {% block modal_size %}modal-lg{% endblock %}" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
{% block form_before %}{% endblock %}
|
{% block form_before %}{% endblock %}
|
||||||
{% block modal_before %}{{ form_start(form) }}{% endblock %}
|
{% block modal_before %}{{ form_start(form, formStartOptions|default({})) }}{% endblock %}
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">×</span></button>
|
||||||
<h4 class="modal-title" id="{{ block('modal_id') }}_label">
|
<h4 class="modal-title" id="{{ block('modal_id') }}_label">
|
||||||
@@ -28,7 +28,9 @@ REQUIRED CHANGES NEED TO BE DONE IN THESE FILES:
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
{% block modal_footer %}
|
{% block modal_footer %}
|
||||||
<button type="submit" class="btn btn-primary pull-left modal-form-save" data-loading-text="{{ 'action.save'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'action.save'|trans }}</button>
|
{% block submit_button %}
|
||||||
|
<button type="submit" class="btn btn-primary pull-left modal-form-save" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" id="{{ block('modal_id') }}_save">{{ (submit_button|default('action.save'))|trans }}</button>
|
||||||
|
{% endblock %}
|
||||||
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
|
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ REQUIRED CHANGES NEED TO BE DONE IN THESE FILES:
|
|||||||
- default/_form_modal.html.twig
|
- default/_form_modal.html.twig
|
||||||
#}
|
#}
|
||||||
<div class="modal{% if block('modal_class') is defined %} {{ block('modal_class') }}{% endif %}" id="{{ block('modal_id') }}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
|
<div class="modal{% if block('modal_class') is defined %} {{ block('modal_class') }}{% endif %}" id="{{ block('modal_id') }}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog {% block modal_size %}modal-lg{% endblock %}" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
{% if block('modal_before') is defined %}{{ block('modal_before') }}{% endif %}
|
{% if block('modal_before') is defined %}{{ block('modal_before') }}{% endif %}
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|||||||
@@ -211,7 +211,7 @@
|
|||||||
<h4><i class="icon {{ icon|icon(icon) }}"></i> {{ title|trans }}</h4>
|
<h4><i class="icon {{ icon|icon(icon) }}"></i> {{ title|trans }}</h4>
|
||||||
{{ description|trans }}
|
{{ description|trans }}
|
||||||
{% elseif icon %}
|
{% elseif icon %}
|
||||||
<h4><i class="icon {{ icon|icon(icon) }}"></i> {{ description|trans }}</h4>
|
<i class="icon {{ icon|icon(icon) }}"></i> {{ description|trans }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ description|trans }}
|
{{ description|trans }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
31
templates/timesheet/layout-export.html.twig
Normal file
31
templates/timesheet/layout-export.html.twig
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'menu.export'|trans }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% if stats.count > 1000 %}
|
||||||
|
{{ widgets.alert('warning', ('export.warn_result_amount'|trans({'%count%': stats.count})), null, 'warning') }}
|
||||||
|
{% endif %}
|
||||||
|
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
|
||||||
|
{% set formOptions = {
|
||||||
|
'title': 'menu.export'|trans,
|
||||||
|
'form': form,
|
||||||
|
'back': path(route_back),
|
||||||
|
'formStartOptions': {'attr': {'class': 'form-narrow searchform', 'target': '_blank'}},
|
||||||
|
} %}
|
||||||
|
{% embed formEditTemplate with formOptions %}
|
||||||
|
{% form_theme form 'form/search-form.html.twig' %}
|
||||||
|
{% block modal_size %}{% endblock %}
|
||||||
|
{% block form_body %}
|
||||||
|
{{ form_widget(form) }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block submit_button %}
|
||||||
|
<div class="btn-group pull-left" role="group">
|
||||||
|
{% for exp in exporter %}
|
||||||
|
<button id="export-btn-{{ exp.id }}" formtarget="_blank" type="submit" name="exporter" value="{{ exp.id }}" class="btn btn-primary" data-loading-text="{{ ('button.' ~ exp.id)|trans }}…">{{ ('button.' ~ exp.id)|trans }}</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% endembed %}
|
||||||
|
{% endblock %}
|
||||||
@@ -41,11 +41,8 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$this->assertHasNoEntriesWithFilter($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
$this->assertPageActions($client, [
|
$this->assertPageActions($client, [
|
||||||
'search' => '#',
|
'search' => '#',
|
||||||
'toolbar-action exporter-csv' => $this->createUrl('/timesheet/export/csv'),
|
|
||||||
'toolbar-action exporter-print' => $this->createUrl('/timesheet/export/print'),
|
|
||||||
'toolbar-action exporter-pdf' => $this->createUrl('/timesheet/export/pdf'),
|
|
||||||
'toolbar-action exporter-xlsx' => $this->createUrl('/timesheet/export/xlsx'),
|
|
||||||
'visibility' => '#',
|
'visibility' => '#',
|
||||||
|
'download toolbar-action modal-ajax-form' => $this->createUrl('/timesheet/export/'),
|
||||||
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
|
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
|
||||||
'help' => 'https://www.kimai.org/documentation/timesheet.html'
|
'help' => 'https://www.kimai.org/documentation/timesheet.html'
|
||||||
]);
|
]);
|
||||||
@@ -134,18 +131,17 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||||
$this->importFixture($fixture);
|
$this->importFixture($fixture);
|
||||||
|
|
||||||
$this->request($client, '/timesheet/');
|
$this->request($client, '/timesheet/export/');
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
|
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('form.searchform')->form();
|
$client->submitForm('export-btn-print', [
|
||||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/timesheet/export/print'));
|
'export' => [
|
||||||
$client->submit($form, [
|
'state' => 1,
|
||||||
'state' => 1,
|
'daterange' => $dateRange,
|
||||||
'pageSize' => 25,
|
'customers' => [],
|
||||||
'daterange' => $dateRange,
|
]
|
||||||
'customers' => [],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|||||||
@@ -43,11 +43,8 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertPageActions($client, [
|
$this->assertPageActions($client, [
|
||||||
'search' => '#',
|
'search' => '#',
|
||||||
'toolbar-action exporter-csv' => $this->createUrl('/team/timesheet/export/csv'),
|
|
||||||
'toolbar-action exporter-print' => $this->createUrl('/team/timesheet/export/print'),
|
|
||||||
'toolbar-action exporter-pdf' => $this->createUrl('/team/timesheet/export/pdf'),
|
|
||||||
'toolbar-action exporter-xlsx' => $this->createUrl('/team/timesheet/export/xlsx'),
|
|
||||||
'visibility' => '#',
|
'visibility' => '#',
|
||||||
|
'download toolbar-action modal-ajax-form' => $this->createUrl('/team/timesheet/export/'),
|
||||||
'create-ts modal-ajax-form' => $this->createUrl('/team/timesheet/create'),
|
'create-ts modal-ajax-form' => $this->createUrl('/team/timesheet/create'),
|
||||||
'create-ts-mu modal-ajax-form' => $this->createUrl('/team/timesheet/create_mu'),
|
'create-ts-mu modal-ajax-form' => $this->createUrl('/team/timesheet/create_mu'),
|
||||||
'help' => 'https://www.kimai.org/documentation/timesheet.html'
|
'help' => 'https://www.kimai.org/documentation/timesheet.html'
|
||||||
@@ -146,18 +143,17 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||||
$this->importFixture($fixture);
|
$this->importFixture($fixture);
|
||||||
|
|
||||||
$this->request($client, '/team/timesheet/');
|
$this->request($client, '/team/timesheet/export/');
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
|
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('form.searchform')->form();
|
$client->submitForm('export-btn-print', [
|
||||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export/print'));
|
'export' => [
|
||||||
$client->submit($form, [
|
'state' => 1,
|
||||||
'state' => 1,
|
'daterange' => $dateRange,
|
||||||
'pageSize' => 25,
|
'customers' => [],
|
||||||
'daterange' => $dateRange,
|
]
|
||||||
'customers' => [],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use App\EventSubscriber\Actions\TimesheetsSubscriber;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\EventSubscriber\Actions\AbstractActionsSubscriber
|
* @covers \App\EventSubscriber\Actions\AbstractActionsSubscriber
|
||||||
* @covers \App\EventSubscriber\Actions\AbstractTimesheetsSubscriber
|
|
||||||
* @covers \App\EventSubscriber\Actions\TimesheetsSubscriber
|
* @covers \App\EventSubscriber\Actions\TimesheetsSubscriber
|
||||||
*/
|
*/
|
||||||
class TimesheetsSubscriberTest extends AbstractActionsSubscriberTest
|
class TimesheetsSubscriberTest extends AbstractActionsSubscriberTest
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use App\EventSubscriber\Actions\TimesheetsTeamSubscriber;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\EventSubscriber\Actions\AbstractActionsSubscriber
|
* @covers \App\EventSubscriber\Actions\AbstractActionsSubscriber
|
||||||
* @covers \App\EventSubscriber\Actions\AbstractTimesheetsSubscriber
|
|
||||||
* @covers \App\EventSubscriber\Actions\TimesheetsTeamSubscriber
|
* @covers \App\EventSubscriber\Actions\TimesheetsTeamSubscriber
|
||||||
*/
|
*/
|
||||||
class TimesheetsTeamSubscriberTest extends AbstractActionsSubscriberTest
|
class TimesheetsTeamSubscriberTest extends AbstractActionsSubscriberTest
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
public function getTestModel()
|
public function getTestModel()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['400', '2437.12', ' EUR 1,947.99 ', 7, 5, 1, 2, 2]
|
['400', '2437.12', ' EUR 1,947.99 ', 6, 5, 1, 2, 2]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,6 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
$this->assertTrue(file_exists($file->getRealPath()));
|
$this->assertTrue(file_exists($file->getRealPath()));
|
||||||
$content = file_get_contents($file->getRealPath());
|
$content = file_get_contents($file->getRealPath());
|
||||||
|
|
||||||
$this->assertStringContainsString('"' . $totalDuration . '"', $content);
|
|
||||||
$this->assertStringContainsString('"' . $totalRate . '"', $content);
|
|
||||||
$this->assertStringContainsString('"' . $expectedRate . '"', $content);
|
$this->assertStringContainsString('"' . $expectedRate . '"', $content);
|
||||||
$this->assertEquals($expectedRows, substr_count($content, PHP_EOL));
|
$this->assertEquals($expectedRows, substr_count($content, PHP_EOL));
|
||||||
$this->assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
|
$this->assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
|
||||||
@@ -108,7 +106,7 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
27 => 'ORDER-123',
|
27 => 'ORDER-123',
|
||||||
];
|
];
|
||||||
|
|
||||||
self::assertEquals(7, \count($all));
|
self::assertEquals(6, \count($all));
|
||||||
self::assertEquals($expected, $all[5]);
|
self::assertEquals($expected, $all[5]);
|
||||||
self::assertEquals(\count($expected), \count($all[0]));
|
self::assertEquals(\count($expected), \count($all[0]));
|
||||||
self::assertEquals('foo', $all[4][14]);
|
self::assertEquals('foo', $all[4][14]);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
public function getTestModel()
|
public function getTestModel()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
['400', '2437.12', ' EUR 1,947.99 ', 7, 5, 1, 2, 2]
|
['400', '2437.12', ' EUR 1,947.99 ', 6, 5, 1, 2, 2]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +52,6 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
$this->assertTrue(file_exists($file->getRealPath()));
|
$this->assertTrue(file_exists($file->getRealPath()));
|
||||||
$content = file_get_contents($file->getRealPath());
|
$content = file_get_contents($file->getRealPath());
|
||||||
|
|
||||||
$this->assertStringContainsString('"' . $totalDuration . '"', $content);
|
|
||||||
$this->assertStringContainsString('"' . $totalRate . '"', $content);
|
|
||||||
$this->assertStringContainsString('"' . $expectedRate . '"', $content);
|
$this->assertStringContainsString('"' . $expectedRate . '"', $content);
|
||||||
$this->assertEquals($expectedRows, substr_count($content, PHP_EOL));
|
$this->assertEquals($expectedRows, substr_count($content, PHP_EOL));
|
||||||
$this->assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
|
$this->assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
|
||||||
@@ -105,7 +103,7 @@ class CsvRendererTest extends AbstractRendererTest
|
|||||||
27 => 'ORDER-123',
|
27 => 'ORDER-123',
|
||||||
];
|
];
|
||||||
|
|
||||||
self::assertEquals(7, \count($all));
|
self::assertEquals(6, \count($all));
|
||||||
self::assertEquals($expected, $all[5]);
|
self::assertEquals($expected, $all[5]);
|
||||||
self::assertEquals(\count($expected), \count($all[0]));
|
self::assertEquals(\count($expected), \count($all[0]));
|
||||||
self::assertEquals('foo', $all[4][14]);
|
self::assertEquals('foo', $all[4][14]);
|
||||||
|
|||||||
26
tests/Repository/Result/TimesheetResultStatisticTest.php
Normal file
26
tests/Repository/Result/TimesheetResultStatisticTest.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Repository\Result;
|
||||||
|
|
||||||
|
use App\Repository\Result\TimesheetResultStatistic;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Repository\Result\TimesheetResultStatistic
|
||||||
|
*/
|
||||||
|
class TimesheetResultStatisticTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testConstruct()
|
||||||
|
{
|
||||||
|
$sut = new TimesheetResultStatistic(13, 7705);
|
||||||
|
self::assertSame(13, $sut->getCount());
|
||||||
|
self::assertSame(7705, $sut->getDuration());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1122,6 +1122,10 @@
|
|||||||
<source>export.date_copyright</source>
|
<source>export.date_copyright</source>
|
||||||
<target>Erstellt %date% mit %kimai%</target>
|
<target>Erstellt %date% mit %kimai%</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="export.warn_result_amount">
|
||||||
|
<source>export.warn_result_amount</source>
|
||||||
|
<target>Ihre Suche führt zu %count% Ergebnissen. Sollte der Export fehlschlagen, müssen Sie die Suche weiter eingrenzen.</target>
|
||||||
|
</trans-unit>
|
||||||
<!--
|
<!--
|
||||||
Navbar - recent entries and activities
|
Navbar - recent entries and activities
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -1122,6 +1122,10 @@
|
|||||||
<source>export.date_copyright</source>
|
<source>export.date_copyright</source>
|
||||||
<target>Created %date% with %kimai%</target>
|
<target>Created %date% with %kimai%</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="export.warn_result_amount">
|
||||||
|
<source>export.warn_result_amount</source>
|
||||||
|
<target>Your search leads to %count% results. If the export fails, you have to narrow down your search further.</target>
|
||||||
|
</trans-unit>
|
||||||
<!--
|
<!--
|
||||||
Navbar - recent entries and activities
|
Navbar - recent entries and activities
|
||||||
-->
|
-->
|
||||||
|
|||||||
Reference in New Issue
Block a user