Update and delete multi timesheets and tags (#1240)
This commit is contained in:
@@ -34,6 +34,7 @@ import KimaiFormSelect from "./plugins/KimaiFormSelect";
|
||||
import KimaiForm from "./plugins/KimaiForm";
|
||||
import KimaiDatePicker from "./plugins/KimaiDatePicker";
|
||||
import KimaiConfirmationLink from "./plugins/KimaiConfirmationLink";
|
||||
import KimaiMultiUpdateTable from "./plugins/KimaiMultiUpdateTable";
|
||||
|
||||
export default class KimaiLoader {
|
||||
|
||||
@@ -67,6 +68,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiAutocomplete('.js-autocomplete'));
|
||||
kimai.registerPlugin(new KimaiForm());
|
||||
kimai.registerPlugin(new KimaiThemeInitializer());
|
||||
kimai.registerPlugin(new KimaiMultiUpdateTable());
|
||||
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
|
||||
|
||||
// notify all listeners that Kimai plugins can now be registered
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class KimaiAlert extends KimaiPlugin {
|
||||
message = translation.get(message);
|
||||
}
|
||||
Swal.fire({
|
||||
type: 'error',
|
||||
icon: 'error',
|
||||
title: title.replace('%reason%', ''),
|
||||
text: message,
|
||||
});
|
||||
@@ -42,10 +42,11 @@ export default class KimaiAlert extends KimaiPlugin {
|
||||
|
||||
Swal.fire({
|
||||
timer: 2000,
|
||||
timerProgressBar: true,
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
showConfirmButton: false,
|
||||
type: 'success',
|
||||
icon: 'success',
|
||||
title: message,
|
||||
});
|
||||
}
|
||||
@@ -65,7 +66,7 @@ export default class KimaiAlert extends KimaiPlugin {
|
||||
|
||||
Swal.fire({
|
||||
title: message,
|
||||
type: 'question',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: translation.get('confirm'),
|
||||
cancelButtonText: translation.get('cancel')
|
||||
|
||||
@@ -30,15 +30,30 @@ export default class KimaiConfirmationLink extends KimaiPlugin {
|
||||
if (target.classList.contains(self.selector)) {
|
||||
const attributes = target.dataset;
|
||||
|
||||
// is this a link?
|
||||
let url = attributes['href'];
|
||||
// or another HTML element with a custom href
|
||||
if (!url) {
|
||||
url = target.getAttribute('href');
|
||||
}
|
||||
|
||||
// or is this a button?
|
||||
let form = null;
|
||||
if (target.type === 'submit' && target.form !== undefined) {
|
||||
form = target.form;
|
||||
}
|
||||
|
||||
if (attributes.question !== undefined) {
|
||||
self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
|
||||
if (value) {
|
||||
if (form === null) {
|
||||
document.location = url;
|
||||
} else {
|
||||
if (url !== null) {
|
||||
form.action = url;
|
||||
}
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export default class KimaiFormSelect extends KimaiPlugin {
|
||||
updateOptions(selectIdentifier, data) {
|
||||
let select = jQuery(selectIdentifier);
|
||||
let emptyOption = jQuery(selectIdentifier + ' option[value=""]');
|
||||
const selectedValue = select.val();
|
||||
|
||||
select.find('option').remove().end().find('optgroup').remove().end();
|
||||
|
||||
@@ -79,6 +80,9 @@ export default class KimaiFormSelect extends KimaiPlugin {
|
||||
select.append(htmlOptions);
|
||||
select.append(emptyOptions);
|
||||
|
||||
// if available, re-select the previous selected option (mostly usable for global activities)
|
||||
select.val(selectedValue);
|
||||
|
||||
// if we don't trigger the change, the other selects won't be resetted
|
||||
select.trigger('change');
|
||||
|
||||
|
||||
73
assets/js/plugins/KimaiMultiUpdateTable.js
Normal file
73
assets/js/plugins/KimaiMultiUpdateTable.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* [KIMAI] KimaiMultiUpdateForm: handle the multi update checkbox list and form
|
||||
*/
|
||||
|
||||
import KimaiPlugin from '../KimaiPlugin';
|
||||
import jQuery from "jquery";
|
||||
|
||||
export default class KimaiMultiUpdateTable extends KimaiPlugin {
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
|
||||
jQuery('body').
|
||||
on('change', '#multi_update_all', function(event) {
|
||||
jQuery('.multi_update_single').prop('checked', jQuery(event.target).prop('checked'));
|
||||
self.toggleForm();
|
||||
})
|
||||
.on('change', '.multi_update_single', function(event) {
|
||||
self.toggleForm();
|
||||
})
|
||||
.on('change', '#multi_update_table_action', function(event) {
|
||||
const selectedItem = jQuery('#multi_update_table_action option:selected');
|
||||
const selectedVal = selectedItem.val();
|
||||
|
||||
if (selectedVal === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = jQuery('#multi_update_form form');
|
||||
const selectedText = selectedItem.text();
|
||||
const ids = self.getSelectedIds();
|
||||
const question = form.attr('data-question').replace(/%action%/, selectedText).replace(/%count%/, ids.length);
|
||||
|
||||
self.getContainer().getPlugin('alert').question(question, function(value) {
|
||||
if (value) {
|
||||
form.attr('action', selectedVal).submit();
|
||||
} else {
|
||||
jQuery('#multi_update_table_action').val('').trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedIds()
|
||||
{
|
||||
let ids = [];
|
||||
jQuery('.multi_update_single:checked').each(function(i){
|
||||
ids[i] = $(this).val();
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
toggleForm()
|
||||
{
|
||||
const ids = this.getSelectedIds();
|
||||
jQuery('#multi_update_table_entities').val(ids.join(','));
|
||||
|
||||
if (ids.length > 0) {
|
||||
jQuery('#multi_update_form').show();
|
||||
} else {
|
||||
jQuery('#multi_update_form').hide();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,10 +33,14 @@ table.dataTable {
|
||||
&.summary td {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
&.open-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
th.multiCheckbox {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
td {
|
||||
/*
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"sass-loader": "^7.0.2",
|
||||
"select2": "^4.0.10",
|
||||
"select2-bootstrap-theme": "^0.1.0-beta.10",
|
||||
"sweetalert2": "^8.11.7",
|
||||
"sweetalert2": "^9.3",
|
||||
"webpack-notifier": "^1.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
public/build/app.120bf1d1.js
Normal file
1
public/build/app.120bf1d1.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,10 +5,10 @@
|
||||
"build/runtime.4ee6be68.js",
|
||||
"build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js",
|
||||
"build/app.09cc993e.js"
|
||||
"build/app.120bf1d1.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.4bdd4f2d.css"
|
||||
"build/app.ca6a8b7f.css"
|
||||
]
|
||||
},
|
||||
"chart": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"build/runtime.4ee6be68.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.a87622f3.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
|
||||
"build/1.c1bee41f.js": "sha384-7UVWcP6Hefp2k/CrtGSITKXx4dSZqtvpAiU8WX7dClETkzMewrUjoCRtVXQ5j3KI",
|
||||
"build/app.09cc993e.js": "sha384-d/JgeK3j55PlnEKB+jFbHt0967vhUr5uwSRapKwFCpWoxGkahBuRz1rgkysgTy+g",
|
||||
"build/app.4bdd4f2d.css": "sha384-XpqGJV9ny/Fk0uJHisAZrAZfWfkzDvm5RJQOaa9ozf6nJstSXyEHziVAza9fIifm",
|
||||
"build/app.120bf1d1.js": "sha384-2Eohdb33hChqGmyJIUGgZFiqzPfUDIv74IoMAX+09VozhJDD/GtPIg5+N2CCOACl",
|
||||
"build/app.ca6a8b7f.css": "sha384-NrITZnNaaIkzEjZkOjxciIbq6V6dKC9VlbJfxliLc2vi8n0113/yLfhbwi5rjzg2",
|
||||
"build/2.7be60d8d.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
|
||||
"build/chart.0af3f813.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
"build/calendar.6f94d071.js": "sha384-CnbCkgF1mbGOycK2A78CF1ZsQ4O07ol9U4gB0Tiec0wKu1mNw8lE22lvoRNzYs/8",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"build/0.a87622f3.js": "build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js": "build/1.c1bee41f.js",
|
||||
"build/2.7be60d8d.js": "build/2.7be60d8d.js",
|
||||
"build/app.css": "build/app.4bdd4f2d.css",
|
||||
"build/app.js": "build/app.09cc993e.js",
|
||||
"build/app.css": "build/app.ca6a8b7f.css",
|
||||
"build/app.js": "build/app.120bf1d1.js",
|
||||
"build/calendar.css": "build/calendar.36ef2a02.css",
|
||||
"build/calendar.js": "build/calendar.6f94d071.js",
|
||||
"build/chart.js": "build/chart.0af3f813.js",
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Form\MultiUpdate\MultiUpdateTable;
|
||||
use App\Form\MultiUpdate\MultiUpdateTableDTO;
|
||||
use App\Form\TagEditForm;
|
||||
use App\Form\Toolbar\TagToolbarForm;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -54,6 +57,7 @@ class TagController extends AbstractController
|
||||
'tags' => $tags,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'multiUpdateForm' => $this->getMultiUpdateForm($repository)->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -119,6 +123,41 @@ class TagController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-delete", name="tags_multi_delete", methods={"POST"})
|
||||
* @Security("is_granted('delete_tag')")
|
||||
*/
|
||||
public function multiDelete(TagRepository $repository, Request $request)
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
$repository->multiDelete($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
}
|
||||
|
||||
protected function getMultiUpdateForm(TagRepository $repository): FormInterface
|
||||
{
|
||||
$dto = new MultiUpdateTableDTO();
|
||||
$dto->addDelete($this->generateUrl('tags_multi_delete'));
|
||||
|
||||
return $this->createForm(MultiUpdateTable::class, $dto, [
|
||||
'action' => $this->generateUrl('tags'),
|
||||
'repository' => $repository,
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TagQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
@@ -16,6 +16,10 @@ use App\Entity\Timesheet;
|
||||
use App\Event\TimesheetMetaDefinitionEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Form\MultiUpdate\MultiUpdateTable;
|
||||
use App\Form\MultiUpdate\MultiUpdateTableDTO;
|
||||
use App\Form\MultiUpdate\TimesheetMultiUpdate;
|
||||
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
@@ -130,6 +134,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
'page' => $query->getPage(),
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'multiUpdateForm' => $this->getMultiUpdateActionForm()->createView(),
|
||||
'showSummary' => $this->includeSummary(),
|
||||
'showStartEndTime' => $this->canSeeStartEndTime(),
|
||||
'metaColumns' => $this->findMetaColumns($query, $location),
|
||||
@@ -174,6 +179,24 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getTags(TagRepository $tagRepository, $tagNames)
|
||||
{
|
||||
$tags = [];
|
||||
if (!is_array($tagNames)) {
|
||||
$tagNames = explode(',', $tagNames);
|
||||
}
|
||||
foreach ($tagNames as $tagName) {
|
||||
$tag = $tagRepository->findTagByName($tagName);
|
||||
if (!$tag) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($tagName);
|
||||
}
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
protected function create(Request $request, string $renderTemplate, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository): Response
|
||||
{
|
||||
$entry = new Timesheet();
|
||||
@@ -190,13 +213,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
|
||||
if ($request->query->get('tags')) {
|
||||
$tagNames = explode(',', $request->query->get('tags'));
|
||||
foreach ($tagNames as $tagName) {
|
||||
$tag = $tagRepository->findTagByName($tagName);
|
||||
if (!$tag) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($tagName);
|
||||
}
|
||||
foreach ($this->getTags($tagRepository, $request->query->get('tags')) as $tag) {
|
||||
$entry->addTag($tag);
|
||||
}
|
||||
}
|
||||
@@ -267,11 +284,142 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return $exporter->render($entries, $query);
|
||||
}
|
||||
|
||||
protected function multiUpdate(Request $request, string $renderTemplate)
|
||||
{
|
||||
$dto = new TimesheetMultiUpdateDTO();
|
||||
|
||||
// initial request from the listing posts a different form
|
||||
$form = $this->getMultiUpdateActionForm();
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$dto->setEntities($form->getData()->getEntities());
|
||||
}
|
||||
|
||||
$form = $this->getMultiUpdateForm($dto);
|
||||
$form->handleRequest($request);
|
||||
|
||||
// remove all, which are not allowed to be edited
|
||||
$timesheets = [];
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($dto->getEntities() as $timesheet) {
|
||||
if (!$this->isGranted('edit', $timesheet)) {
|
||||
continue;
|
||||
}
|
||||
$timesheets[] = $timesheet;
|
||||
}
|
||||
$dto->setEntities($timesheets);
|
||||
|
||||
if (count($dto->getEntities()) === 0) {
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var Timesheet $timesheet */
|
||||
$execute = false;
|
||||
foreach ($dto->getEntities() as $timesheet) {
|
||||
if ($dto->isReplaceTags()) {
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$timesheet->removeTag($tag);
|
||||
}
|
||||
$execute = true;
|
||||
}
|
||||
foreach ($dto->getTags() as $tag) {
|
||||
$timesheet->addTag($tag);
|
||||
$execute = true;
|
||||
}
|
||||
if (null !== $dto->getActivity()) {
|
||||
$timesheet->setActivity($dto->getActivity());
|
||||
$execute = true;
|
||||
}
|
||||
if (null !== $dto->getProject()) {
|
||||
$timesheet->setProject($dto->getProject());
|
||||
$execute = true;
|
||||
}
|
||||
if (null !== $dto->getUser()) {
|
||||
$timesheet->setUser($dto->getUser());
|
||||
$execute = true;
|
||||
}
|
||||
if (null !== $dto->isExported()) {
|
||||
$timesheet->setExported($dto->isExported());
|
||||
$execute = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($execute) {
|
||||
try {
|
||||
$this->repository->saveMultiple($dto->getEntities());
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
'form' => $form->createView(),
|
||||
'dto' => $dto,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function multiDelete(Request $request)
|
||||
{
|
||||
$form = $this->getMultiUpdateActionForm();
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$dto = $form->getData();
|
||||
$timesheets = [];
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($dto->getEntities() as $timesheet) {
|
||||
if (!$this->isGranted('delete', $timesheet)) {
|
||||
continue;
|
||||
}
|
||||
$timesheets[] = $timesheet;
|
||||
}
|
||||
$dto->setEntities($timesheets);
|
||||
|
||||
try {
|
||||
$this->repository->deleteMultiple($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
{
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
|
||||
protected function getMultiUpdateForm(TimesheetMultiUpdateDTO $multiUpdate): FormInterface
|
||||
{
|
||||
return $this->createForm(TimesheetMultiUpdate::class, $multiUpdate, [
|
||||
'action' => $this->generateUrl($this->getMultiUpdateRoute(), []),
|
||||
'method' => 'POST',
|
||||
'include_exported' => $this->isGranted($this->getPermissionEditExport()),
|
||||
'include_user' => $this->includeUserInForms(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getMultiUpdateActionForm(): FormInterface
|
||||
{
|
||||
$dto = new MultiUpdateTableDTO();
|
||||
|
||||
$dto->addUpdate($this->generateUrl($this->getMultiUpdateRoute()));
|
||||
$dto->addDelete($this->generateUrl($this->getMultiDeleteRoute()));
|
||||
|
||||
return $this->createForm(MultiUpdateTable::class, $dto, [
|
||||
'action' => $this->generateUrl($this->getTimesheetRoute()),
|
||||
'repository' => $this->getRepository(),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||
{
|
||||
return $this->createForm($this->getCreateFormClassName(), $entry, [
|
||||
@@ -325,6 +473,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getPermissionEditExport(): string
|
||||
{
|
||||
return 'edit_export_own_timesheet';
|
||||
}
|
||||
|
||||
protected function getCreateFormClassName(): string
|
||||
{
|
||||
return TimesheetEditForm::class;
|
||||
@@ -360,6 +513,16 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return 'timesheet_create';
|
||||
}
|
||||
|
||||
protected function getMultiUpdateRoute(): string
|
||||
{
|
||||
return 'timesheet_multi_update';
|
||||
}
|
||||
|
||||
protected function getMultiDeleteRoute(): string
|
||||
{
|
||||
return 'timesheet_multi_delete';
|
||||
}
|
||||
|
||||
protected function canSeeStartEndTime(): bool
|
||||
{
|
||||
return $this->getTrackingMode()->canSeeBeginAndEndTimes();
|
||||
|
||||
@@ -65,6 +65,24 @@ class TimesheetController extends TimesheetAbstractController
|
||||
return $this->edit($entry, $request, 'timesheet/edit.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-update", name="timesheet_multi_update", methods={"POST"})
|
||||
* @Security("is_granted('edit_own_timesheet')")
|
||||
*/
|
||||
public function multiUpdateAction(Request $request)
|
||||
{
|
||||
return $this->multiUpdate($request, 'timesheet/multi-update.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-delete", name="timesheet_multi_delete", methods={"POST"})
|
||||
* @Security("is_granted('delete_own_timesheet')")
|
||||
*/
|
||||
public function multiDeleteAction(Request $request)
|
||||
{
|
||||
return $this->multiDelete($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
|
||||
@@ -80,11 +80,34 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-update", name="admin_timesheet_multi_update", methods={"POST"})
|
||||
* @Security("is_granted('edit_other_timesheet')")
|
||||
*/
|
||||
public function multiUpdateAction(Request $request)
|
||||
{
|
||||
return $this->multiUpdate($request, 'timesheet-team/multi-update.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/multi-delete", name="admin_timesheet_multi_delete", methods={"POST"})
|
||||
* @Security("is_granted('delete_other_timesheet')")
|
||||
*/
|
||||
public function multiDeleteAction(Request $request)
|
||||
{
|
||||
return $this->multiDelete($request);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
{
|
||||
$query->setCurrentUser($this->getUser());
|
||||
}
|
||||
|
||||
protected function getPermissionEditExport(): string
|
||||
{
|
||||
return 'edit_export_other_timesheet';
|
||||
}
|
||||
|
||||
protected function getCreateFormClassName(): string
|
||||
{
|
||||
return TimesheetAdminEditForm::class;
|
||||
@@ -115,6 +138,16 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
return 'admin_timesheet_create';
|
||||
}
|
||||
|
||||
protected function getMultiUpdateRoute(): string
|
||||
{
|
||||
return 'admin_timesheet_multi_update';
|
||||
}
|
||||
|
||||
protected function getMultiDeleteRoute(): string
|
||||
{
|
||||
return 'admin_timesheet_multi_delete';
|
||||
}
|
||||
|
||||
protected function canSeeStartEndTime(): bool
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -63,7 +63,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
if ($auth->isGranted('view_own_timesheet')) {
|
||||
$timesheets = new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], $this->getIcon('timesheet'));
|
||||
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create']);
|
||||
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
|
||||
$menu->addItem($timesheets);
|
||||
$menu->addItem(
|
||||
new MenuItemModel('calendar', 'calendar.title', 'calendar', [], $this->getIcon('calendar'))
|
||||
@@ -89,7 +89,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
if ($auth->isGranted('view_other_timesheet')) {
|
||||
$timesheets = new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], $this->getIcon('timesheet-team'));
|
||||
$timesheets->setChildRoutes(['admin_timesheet_export', 'admin_timesheet_edit', 'admin_timesheet_create']);
|
||||
$timesheets->setChildRoutes(['admin_timesheet_export', 'admin_timesheet_edit', 'admin_timesheet_create', 'admin_timesheet_multi_update']);
|
||||
$menu->addChild($timesheets);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,12 @@ class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
$view->vars['attr'] = [];
|
||||
}
|
||||
|
||||
$view->vars['attr'] = array_merge(
|
||||
$view->vars['attr'],
|
||||
['class' => 'selectpicker', 'data-live-search' => true, 'data-width' => '100%']
|
||||
);
|
||||
$extendedOptions = ['class' => 'selectpicker', 'data-width' => '100%'];
|
||||
if (!$options['search']) {
|
||||
$extendedOptions['data-minimum-results-for-search'] = 'Infinity';
|
||||
}
|
||||
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], $extendedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,5 +80,9 @@ class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
$resolver->setDefined(['selectpicker']);
|
||||
$resolver->setAllowedTypes('selectpicker', 'boolean');
|
||||
$resolver->setDefault('selectpicker', true);
|
||||
|
||||
$resolver->setDefined(['search']);
|
||||
$resolver->setAllowedTypes('search', 'boolean');
|
||||
$resolver->setDefault('search', true);
|
||||
}
|
||||
}
|
||||
|
||||
76
src/Form/MultiUpdate/MultiUpdateTable.php
Normal file
76
src/Form/MultiUpdate/MultiUpdateTable.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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\MultiUpdate;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class MultiUpdateTable extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var EntityRepository $repository */
|
||||
$repository = $options['repository'];
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $options['data'];
|
||||
|
||||
$builder->add('entities', HiddenType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->get('entities')->addModelTransformer(
|
||||
new CallbackTransformer(
|
||||
function ($ids) {
|
||||
return implode(',', $ids);
|
||||
},
|
||||
function ($ids) use ($repository) {
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $repository->matching((new Criteria())->where(Criteria::expr()->in('id', explode(',', $ids))));
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
$builder->add('action', ChoiceType::class, [
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
'choices' => $dto->getActions(),
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefined(['repository']);
|
||||
$resolver->setAllowedTypes('repository', EntityRepository::class);
|
||||
$resolver->setRequired('repository');
|
||||
|
||||
$resolver->setDefaults([
|
||||
'data_class' => MultiUpdateTableDTO::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'entities_multiupdate',
|
||||
]);
|
||||
}
|
||||
}
|
||||
86
src/Form/MultiUpdate/MultiUpdateTableDTO.php
Normal file
86
src/Form/MultiUpdate/MultiUpdateTableDTO.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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\MultiUpdate;
|
||||
|
||||
class MultiUpdateTableDTO
|
||||
{
|
||||
/**
|
||||
* @var object[]
|
||||
*/
|
||||
private $entities = [];
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $actions = ['' => ''];
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $action = null;
|
||||
|
||||
/**
|
||||
* @return object[]
|
||||
*/
|
||||
public function getEntities(): iterable
|
||||
{
|
||||
return $this->entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object[] $entities
|
||||
* @return MultiUpdateTableDTO
|
||||
*/
|
||||
public function setEntities(iterable $entities): MultiUpdateTableDTO
|
||||
{
|
||||
$this->entities = $entities;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getActions(): array
|
||||
{
|
||||
return $this->actions;
|
||||
}
|
||||
|
||||
public function addAction(string $label, string $url): MultiUpdateTableDTO
|
||||
{
|
||||
$this->actions[$label] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDelete(string $url): MultiUpdateTableDTO
|
||||
{
|
||||
$this->actions['action.delete'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUpdate(string $url): MultiUpdateTableDTO
|
||||
{
|
||||
$this->actions['action.edit'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAction(): ?string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function setAction(string $action): MultiUpdateTableDTO
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
232
src/Form/MultiUpdate/TimesheetMultiUpdate.php
Normal file
232
src/Form/MultiUpdate/TimesheetMultiUpdate.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?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\MultiUpdate;
|
||||
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TimesheetMultiUpdate extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $timesheet;
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $customers;
|
||||
|
||||
public function __construct(TimesheetRepository $timesheet, CustomerRepository $customer)
|
||||
{
|
||||
$this->timesheet = $timesheet;
|
||||
$this->customers = $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$activity = null;
|
||||
$project = null;
|
||||
$customer = null;
|
||||
$customerCount = $this->customers->countCustomer(true);
|
||||
|
||||
if (isset($options['data'])) {
|
||||
/** @var TimesheetMultiUpdateDTO $entry */
|
||||
$entry = $options['data'];
|
||||
|
||||
$activity = $entry->getActivity();
|
||||
$project = $entry->getProject();
|
||||
$customer = null === $project ? null : $project->getCustomer();
|
||||
|
||||
if (null === $project && null !== $activity) {
|
||||
$project = $activity->getProject();
|
||||
}
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'mapped' => false,
|
||||
'project_enabled' => true,
|
||||
])
|
||||
;
|
||||
|
||||
$projectOptions = [];
|
||||
|
||||
if ($customerCount < 2) {
|
||||
$projectOptions['group_by'] = null;
|
||||
}
|
||||
|
||||
$builder
|
||||
->add(
|
||||
'project',
|
||||
ProjectType::class,
|
||||
array_merge($projectOptions, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($builder, $project, $customer) {
|
||||
$data = $event->getData();
|
||||
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
||||
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
$builder
|
||||
->add('activity', ActivityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity, $project) {
|
||||
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $project));
|
||||
},
|
||||
])
|
||||
;
|
||||
|
||||
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($activity) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['project']) || empty($data['project'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data, $activity) {
|
||||
return $repo->getQueryBuilderForFormType(new ActivityFormTypeQuery($activity, $data['project']));
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
$builder->add('replaceTags', ChoiceType::class, [
|
||||
'label' => false,
|
||||
'required' => true,
|
||||
'expanded' => true,
|
||||
'choices' => [
|
||||
'label.replaceTags' => true,
|
||||
'label.appendTags' => false,
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('tags', TagsInputType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['include_exported']) {
|
||||
$builder->add('exported', YesNoType::class, [
|
||||
'label' => 'label.exported'
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->add('entities', HiddenType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->get('entities')->addModelTransformer(
|
||||
new CallbackTransformer(
|
||||
function ($timesheets) {
|
||||
$ids = [];
|
||||
/** @var \App\Entity\Timesheet $timesheet */
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$ids[] = $timesheet->getId();
|
||||
}
|
||||
|
||||
return implode(',', $ids);
|
||||
},
|
||||
function ($ids) {
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->timesheet->matching((new Criteria())->where(Criteria::expr()->in('id', explode(',', $ids))));
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => TimesheetMultiUpdateDTO::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'timesheet_multiupdate',
|
||||
'include_user' => false,
|
||||
'include_exported' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
139
src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php
Normal file
139
src/Form/MultiUpdate/TimesheetMultiUpdateDTO.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?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\MultiUpdate;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
/**
|
||||
* @App\Validator\Constraints\TimesheetMultiUpdate
|
||||
*/
|
||||
class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO
|
||||
{
|
||||
/**
|
||||
* @var Tag[]|ArrayCollection|iterable
|
||||
*/
|
||||
private $tags = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $replaceTags = false;
|
||||
/**
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var Project|null
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var Activity|null
|
||||
*/
|
||||
private $activity;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $exported = null;
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActivity(): ?Activity
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
public function setActivity(Activity $activity): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tag[]|ArrayCollection|iterable
|
||||
*/
|
||||
public function getTags(): iterable
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function setTags(iterable $tags): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->tags = $tags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isExported(): ?bool
|
||||
{
|
||||
return $this->exported;
|
||||
}
|
||||
|
||||
public function setExported(bool $exported): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->exported = $exported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isReplaceTags(): bool
|
||||
{
|
||||
return $this->replaceTags;
|
||||
}
|
||||
|
||||
public function setReplaceTags(bool $replaceTags): TimesheetMultiUpdateDTO
|
||||
{
|
||||
$this->replaceTags = $replaceTags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'label' => $label,
|
||||
'search' => false
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -110,6 +111,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
'required' => false,
|
||||
'search' => false
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -218,6 +220,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO add a system setting to control if tags can be created on the fly
|
||||
protected function addTagSelectField(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('tags', TagsSelectType::class, [
|
||||
@@ -237,6 +240,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'search' => false,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
@@ -251,6 +255,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'label' => 'label.exported',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'search' => false,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||
|
||||
@@ -48,6 +48,14 @@ final class UserIdLoader implements LoaderInterface
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL u.{id}', 'preferences')
|
||||
->from(User::class, 'u')
|
||||
->leftJoin('u.preferences', 'preferences')
|
||||
->andWhere($qb->expr()->in('u.id', $ids))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$teamIds = [];
|
||||
foreach ($users as $user) {
|
||||
foreach ($user->getTeams() as $team) {
|
||||
|
||||
@@ -28,23 +28,14 @@ class UserQuery extends VisibilityQuery
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRole()
|
||||
public function getRole(): ?string
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $role
|
||||
* @return UserQuery
|
||||
*/
|
||||
public function setRole($role)
|
||||
public function setRole(?string $role): UserQuery
|
||||
{
|
||||
if (null === $role || false !== strpos($role, 'ROLE_')) {
|
||||
$this->role = $role;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -159,4 +159,25 @@ class TagRepository extends EntityRepository
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag[] $tags
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function multiDelete(iterable $tags): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($tags as $tag) {
|
||||
$em->remove($tag);
|
||||
}
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,27 @@ class TimesheetRepository extends EntityRepository
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deleteMultiple(iterable $timesheets): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$em->remove($timesheet);
|
||||
}
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
@@ -79,6 +100,27 @@ class TimesheetRepository extends EntityRepository
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function saveMultiple(array $timesheets): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$em->persist($timesheet);
|
||||
}
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @return bool
|
||||
|
||||
43
src/Validator/Constraints/TimesheetMultiUpdate.php
Normal file
43
src/Validator/Constraints/TimesheetMultiUpdate.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
|
||||
*/
|
||||
class TimesheetMultiUpdate extends Constraint
|
||||
{
|
||||
public const MISSING_ACTIVITY_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d84';
|
||||
public const MISSING_PROJECT_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d85';
|
||||
public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'xy5hffg-dsfef3-426a-83d7-1f2d33hs5d86';
|
||||
public const DISABLED_ACTIVITY_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d87';
|
||||
public const DISABLED_PROJECT_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d88';
|
||||
public const DISABLED_CUSTOMER_ERROR = 'yd5hffg-dsfef3-426a-83d7-1f2d33hs5d89';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::MISSING_ACTIVITY_ERROR => 'A timesheet must have an activity.',
|
||||
self::MISSING_PROJECT_ERROR => 'A timesheet must have a project.',
|
||||
self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch: chosen project does not match the activity project.',
|
||||
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
|
||||
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
|
||||
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
|
||||
];
|
||||
|
||||
public $message = 'This form has invalid settings.';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
122
src/Validator/Constraints/TimesheetMultiUpdateValidator.php
Normal file
122
src/Validator/Constraints/TimesheetMultiUpdateValidator.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?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\Validator\Constraints;
|
||||
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
|
||||
use App\Validator\Constraints\TimesheetMultiUpdate as TimesheetConstraint;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class TimesheetMultiUpdateValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $auth;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* @param AuthorizationCheckerInterface $auth
|
||||
* @param TimesheetConfiguration $configuration
|
||||
*/
|
||||
public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetMultiUpdateDTO|mixed $value
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!($constraint instanceof TimesheetConstraint)) {
|
||||
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\TimesheetMultiUpdate');
|
||||
}
|
||||
|
||||
if (!is_object($value) || !($value instanceof TimesheetMultiUpdateDTO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validateActivityAndProject($value, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetMultiUpdateDTO $dto
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActivityAndProject(TimesheetMultiUpdateDTO $dto, ExecutionContextInterface $context)
|
||||
{
|
||||
$activity = $dto->getActivity();
|
||||
$project = $dto->getProject();
|
||||
|
||||
// non global activity without project
|
||||
if (null !== $activity && null !== $activity->getProject() && null === $project) {
|
||||
$context->buildViolation('Missing project')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::MISSING_PROJECT_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
// only project was chosen
|
||||
if (null === $activity && null !== $project) {
|
||||
$context->buildViolation('Missing activity')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::MISSING_ACTIVITY_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $activity) {
|
||||
if (null !== $activity->getProject() && $activity->getProject() !== $project) {
|
||||
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::ACTIVITY_PROJECT_MISMATCH_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (!$activity->isVisible()) {
|
||||
$context->buildViolation('Cannot assign a disabled activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::DISABLED_ACTIVITY_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $project) {
|
||||
if (!$project->isVisible()) {
|
||||
$context->buildViolation('Cannot assign a disabled project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::DISABLED_PROJECT_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (!$project->getCustomer()->isVisible()) {
|
||||
$context->buildViolation('Cannot assign a disabled customer.')
|
||||
->atPath('customer')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::DISABLED_CUSTOMER_ERROR)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@
|
||||
{{ macro.data_table_header_options(name, columns, {'skipStripped': skipStripped, 'reloadEvent': reloadEvent}) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro data_table_footer(entries, route) %}
|
||||
{% macro data_table_footer(entries, route, multi_update_form) %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -192,6 +192,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if multi_update_form is not null %}
|
||||
<div id="multi_update_form" style="display: none">
|
||||
{{ form_start(multi_update_form, {'attr': {'data-question': 'update_multiple'|trans}}) }}
|
||||
<label class="col-sm-1 col-xs-2 control-label" for="multi_update_table_action">
|
||||
<span class="pull-left"><i class="fas fa-level-up-alt fa-2x fa-rotate-90"></i></span>
|
||||
</label>
|
||||
<div class="col-sm-11 col-xs-10">
|
||||
{{ form_widget(multi_update_form.action) }}
|
||||
</div>
|
||||
{{ form_end(multi_update_form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if route is not empty and entries is not null%}
|
||||
<div class="navigation text-center no-print">
|
||||
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { proximity: 1, routeName: route }) }}
|
||||
@@ -207,3 +219,11 @@
|
||||
{{ widgets.form_type_value(metaField.type, metaField.value, entry) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro datatable_multiupdate_all() %}
|
||||
<input type="checkbox" id="multi_update_all" class="multiupdater">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro datatable_multiupdate_row(id) %}
|
||||
<input type="checkbox" name="id" value="{{ id }}" class="multi_update_single multiupdater">
|
||||
{% endmacro %}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
{% else %}
|
||||
|
||||
{% set columns = {
|
||||
'id': {'class': 'alwaysVisible multiCheckbox', 'orderBy': false, 'title': false, 'html_before': tables.datatable_multiupdate_all()},
|
||||
'name': '',
|
||||
'amount': '',
|
||||
'actions': 'actions alwaysVisible',
|
||||
@@ -31,6 +32,7 @@
|
||||
{% endif %}
|
||||
|
||||
<tr{% if manageAllowed %} class="modal-ajax-form open-edit" data-href="{{ path('tags_edit', {'id': tag.id}) }}"{% endif %}>
|
||||
<td class="text-nowrap">{{ tables.datatable_multiupdate_row(tag.id) }}</td>
|
||||
<td>{{ tag.name }}</td>
|
||||
<td>{{ widgets.label(tag.amount, type) }}</td>
|
||||
<td class="actions">
|
||||
@@ -38,7 +40,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{{ tables.data_table_footer(tags, 'tags_paginated') }}
|
||||
{{ tables.data_table_footer(tags, 'tags_paginated', multiUpdateForm) }}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -62,3 +62,14 @@
|
||||
{{ widgets.entity_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timesheets_team_multi_update(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
{% set actions = actions|merge({'back': path('admin_timesheet')}) %}
|
||||
{% set actions = actions|merge({'help': {'url': 'timesheet.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.timesheets_team_multi_update', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,111 +1,16 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% extends 'timesheet/layout-listing.html.twig' %}
|
||||
{% import "timesheet-team/actions.html.twig" as actions %}
|
||||
|
||||
{% set tableName = 'timesheet_admin' %}
|
||||
{% set columns = {
|
||||
'date': {'class': 'alwaysVisible', 'orderBy': 'begin'},
|
||||
} %}
|
||||
|
||||
{% if showStartEndTime %}
|
||||
{% set columns = columns|merge({
|
||||
'starttime': {'class': 'hidden-xs', 'orderBy': 'begin'},
|
||||
'endtime': {'class': 'hidden-xs', 'orderBy': 'end'}
|
||||
}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set columns = columns|merge({
|
||||
'duration': '',
|
||||
'rate': '',
|
||||
'customer': 'hidden-xs hidden-sm hidden-md',
|
||||
'project': 'hidden-xs hidden-sm hidden-md',
|
||||
'activity': 'hidden-xs hidden-sm',
|
||||
'description': 'hidden-xs hidden-sm',
|
||||
'username': {'class': 'hidden-xs', 'orderBy': false},
|
||||
'tags': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||
}) %}
|
||||
{% for field in metaColumns %}
|
||||
{% set columns = columns|merge({
|
||||
('mf_' ~ field.name): {'title': field.label, 'class': 'hidden-xs hidden-sm', 'orderBy': false}
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
{% set paginationRoute = 'admin_timesheet_paginated' %}
|
||||
{% set editRoute = 'admin_timesheet_edit' %}
|
||||
{% set canSeeRate = true %}
|
||||
{% set canSeeUsername = true %}
|
||||
{% set allowMarkdown = false %}
|
||||
|
||||
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.timesheets_team('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.timesheetUpdate'}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{% if not entry.end %} recording{% endif %}" data-href="{{ path('admin_timesheet_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
||||
|
||||
{% if showStartEndTime %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|time }}</td>
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'endtime') }}">
|
||||
{% if entry.end %}
|
||||
{{ entry.end|time }}
|
||||
{% else %}
|
||||
‐
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.end %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
{% else %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">
|
||||
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}" data-format="{{ get_format_duration() }}">{{ entry|duration }}</i>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||
{% if not entry.end or not is_granted('view_rate', entry) %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
{{ widgets.label_customer(entry.project.customer) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||
{{ widgets.label_project(entry.project) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">
|
||||
{{ widgets.label_activity(entry.activity) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ entry.description|nl2br }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">
|
||||
{{ widgets.label_user(entry.user) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'tags') }}">{{ widgets.tag_list(entry.tags) }}</td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="actions">
|
||||
{% block row_action %}
|
||||
{{- actions.timesheet_team(entry, 'index') -}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{{ tables.data_table_footer(entries, 'admin_timesheet_paginated') }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
7
templates/timesheet-team/multi-update.html.twig
Normal file
7
templates/timesheet-team/multi-update.html.twig
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'timesheet/layout-multi-update.html.twig' %}
|
||||
|
||||
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% import "timesheet-team/actions.html.twig" as actions %}
|
||||
{{ actions.timesheets_team_multi_update('multi-update') }}
|
||||
{% endblock %}
|
||||
@@ -64,3 +64,14 @@
|
||||
{% endif %}
|
||||
{%- endapply -%}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro timesheets_multi_update(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {} %}
|
||||
{% set actions = actions|merge({'back': path('timesheet')}) %}
|
||||
{% set actions = actions|merge({'help': {'url': 'timesheet.html'|docu_link, 'target': '_blank'}}) %}
|
||||
|
||||
{% set event = trigger('actions.timesheets_multi_update', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,162 +1,16 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
{% extends 'timesheet/layout-listing.html.twig' %}
|
||||
{% import "timesheet/actions.html.twig" as actions %}
|
||||
{% import _self as timesheet %}
|
||||
|
||||
{% set tableName = 'timesheet' %}
|
||||
{% set paginationRoute = 'timesheet_paginated' %}
|
||||
{% set editRoute = 'timesheet_edit' %}
|
||||
{% set canSeeRate = is_granted('view_rate_own_timesheet') %}
|
||||
{% set columns = {
|
||||
'date': {'class': 'alwaysVisible', 'orderBy': 'begin'},
|
||||
} %}
|
||||
|
||||
{% if showStartEndTime %}
|
||||
{% set columns = columns|merge({
|
||||
'starttime': {'class': '', 'orderBy': 'begin'},
|
||||
'endtime': {'class': 'hidden-xs', 'orderBy': 'end'}
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({'duration': ''}) %}
|
||||
{% if canSeeRate %}
|
||||
{% set columns = columns|merge({'rate': 'hidden-xs'}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'customer': 'hidden-xs hidden-sm hidden-md',
|
||||
'project': 'hidden-xs hidden-sm hidden-md',
|
||||
'activity': 'hidden-xs hidden-sm',
|
||||
'description': 'hidden-xs hidden-sm',
|
||||
'tags': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||
}) %}
|
||||
{% for field in metaColumns %}
|
||||
{% set columns = columns|merge({
|
||||
('mf_' ~ field.name): {'title': field.label, 'class': 'hidden-xs hidden-sm', 'orderBy': false}
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
{% set canSeeUsername = false %}
|
||||
{% set allowMarkdown = true %}
|
||||
|
||||
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.timesheets('index') }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'striped': not showSummary, 'reload': 'kimai.timesheetUpdate'}) }}
|
||||
|
||||
{% set day = null %}
|
||||
{% set dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{% for entry in entries %}
|
||||
{%- set customerCurrency = entry.project.customer.currency -%}
|
||||
{%- if day is same as(null) -%}
|
||||
{% set day = entry.begin|date_short %}
|
||||
{% endif %}
|
||||
{%- if showSummary and day is not same as(entry.begin|date_short) -%}
|
||||
{{ timesheet.summary(day, dayDuration, dayRate, columns, canSeeRate, showStartEndTime, tableName, metaColumns) }}
|
||||
{% set day = entry.begin|date_short %}
|
||||
{% set dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{%- endif -%}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{% if not entry.end %} recording{% endif %}" data-href="{{ path('timesheet_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
||||
|
||||
{% if showStartEndTime %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|time }}</td>
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'endtime') }}">
|
||||
{% if entry.end %}
|
||||
{{ entry.end|time }}
|
||||
{% else %}
|
||||
‐
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.end %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
{% else %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">
|
||||
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}" data-format="{{ get_format_duration() }}">{{ entry|duration }}</i>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% if canSeeRate %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||
{% if not entry.end %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.project.customer) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">{{ widgets.label_project(entry.project) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">{{ entry.description|escape|desc2html }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'tags') }}">{{ widgets.tag_list(entry.tags) }}</td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="actions">
|
||||
{% block row_action %}
|
||||
{{- actions.timesheet(entry, 'index') -}}
|
||||
</td>
|
||||
</tr>
|
||||
{%- if entry.end -%}
|
||||
{% if dayRate[customerCurrency] is not defined %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
|
||||
{% endif %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
|
||||
{%- endif -%}
|
||||
{%- set dayDuration = dayDuration + entry.duration -%}
|
||||
{% endfor %}
|
||||
|
||||
{% if showSummary %}
|
||||
{{ timesheet.summary(day, dayDuration, dayRate, columns, canSeeRate, showStartEndTime, tableName, metaColumns) }}
|
||||
{% endif %}
|
||||
|
||||
{{ tables.data_table_footer(entries, 'timesheet_paginated') }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro summary(day, duration, dayRates, columns, canSeeRate, showStartEndTime, tableName, metaColumns) %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
<tr class="summary info">
|
||||
<td class="text-nowrap">{{ day }}</td>
|
||||
{% if showStartEndTime %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'starttime') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}"></td>
|
||||
{% endif %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ duration|duration }}</td>
|
||||
{% if canSeeRate %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||
{% for currency, rate in dayRates %}
|
||||
{{ rate|money(currency) }}
|
||||
{% if not loop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'tags') }}"></td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}"></td>
|
||||
{% endfor %}
|
||||
<td class="actions"></td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
191
templates/timesheet/layout-listing.html.twig
Normal file
191
templates/timesheet/layout-listing.html.twig
Normal file
@@ -0,0 +1,191 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||
|
||||
{% set showSummary = showSummary|default(false) %}
|
||||
{% set showStartEndTime = showStartEndTime|default(true) %}
|
||||
|
||||
{% set columns = {
|
||||
'id': {'class': 'alwaysVisible multiCheckbox', 'orderBy': false, 'title': false, 'html_before': tables.datatable_multiupdate_all()},
|
||||
'date': {'class': 'alwaysVisible', 'orderBy': 'begin'},
|
||||
} %}
|
||||
|
||||
{% if showStartEndTime %}
|
||||
{% set columns = columns|merge({
|
||||
'starttime': {'class': 'hidden-xs', 'orderBy': 'begin'},
|
||||
'endtime': {'class': 'hidden-xs', 'orderBy': 'end'}
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({'duration': ''}) %}
|
||||
{% if canSeeRate %}
|
||||
{% set columns = columns|merge({'rate': ''}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'customer': 'hidden-xs hidden-sm hidden-md',
|
||||
'project': 'hidden-xs hidden-sm hidden-md',
|
||||
'activity': 'hidden-xs hidden-sm',
|
||||
'description': 'hidden-xs hidden-sm',
|
||||
}) %}
|
||||
{% if canSeeUsername %}
|
||||
{% set columns = columns|merge({
|
||||
'username': {'class': 'hidden-xs', 'orderBy': false},
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% set columns = columns|merge({
|
||||
'tags': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
|
||||
}) %}
|
||||
{% for field in metaColumns %}
|
||||
{% set columns = columns|merge({
|
||||
('mf_' ~ field.name): {'title': field.label, 'class': 'hidden-xs hidden-sm', 'orderBy': false}
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{% set columns = columns|merge({
|
||||
'actions': 'actions alwaysVisible',
|
||||
}) %}
|
||||
|
||||
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% else %}
|
||||
{{ tables.datatable_header(tableName, columns, query, {'striped': not showSummary, 'reload': 'kimai.timesheetUpdate'}) }}
|
||||
|
||||
{% set day = null %}
|
||||
{% set dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{% for entry in entries %}
|
||||
{%- set customerCurrency = entry.project.customer.currency -%}
|
||||
{%- if day is same as(null) -%}
|
||||
{% set day = entry.begin|date_short %}
|
||||
{% endif %}
|
||||
{%- if showSummary and day is not same as(entry.begin|date_short) -%}
|
||||
{{ _self.summary(day, dayDuration, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
|
||||
{% set day = entry.begin|date_short %}
|
||||
{% set dayDuration = 0 %}
|
||||
{% set dayRate = {} %}
|
||||
{%- endif -%}
|
||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{% if not entry.end %} recording{% endif %}" data-href="{{ path(editRoute, {'id': entry.id}) }}"{% endif %}>
|
||||
<td class="text-nowrap">{{ tables.datatable_multiupdate_row(entry.id) }}</td>
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
||||
|
||||
{% if showStartEndTime %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|time }}</td>
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'endtime') }}">
|
||||
{% if entry.end %}
|
||||
{{ entry.end|time }}
|
||||
{% else %}
|
||||
‐
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.end %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
{% else %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">
|
||||
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}" data-format="{{ get_format_duration() }}">{{ entry|duration }}</i>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% if canSeeRate %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||
{% if not entry.end or not is_granted('view_rate', entry) %}
|
||||
‐
|
||||
{% else %}
|
||||
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
{{ widgets.label_customer(entry.project.customer) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||
{{ widgets.label_project(entry.project) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">
|
||||
{{ widgets.label_activity(entry.activity) }}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
|
||||
{% if allowMarkdown %}
|
||||
{{ entry.description|escape|desc2html }}
|
||||
{% else %}
|
||||
{{ entry.description|nl2br }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% if canSeeUsername %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">
|
||||
{{ widgets.label_user(entry.user) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'tags') }}">{{ widgets.tag_list(entry.tags) }}</td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||
{{ tables.datatable_meta_column(entry, field) }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="actions">
|
||||
{{ block('row_action') }}
|
||||
</td>
|
||||
</tr>
|
||||
{%- if entry.end -%}
|
||||
{% if dayRate[customerCurrency] is not defined %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
|
||||
{% endif %}
|
||||
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
|
||||
{%- endif -%}
|
||||
{%- set dayDuration = dayDuration + entry.duration -%}
|
||||
{% endfor %}
|
||||
|
||||
{% if showSummary %}
|
||||
{{ _self.summary(day, dayDuration, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
|
||||
{% endif %}
|
||||
|
||||
{{ tables.data_table_footer(entries, paginationRoute, multiUpdateForm) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro summary(day, duration, dayRates, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
<tr class="summary info">
|
||||
<td></td>
|
||||
<td class="text-nowrap">{{ day }}</td>
|
||||
{% if showStartEndTime %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'starttime') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}"></td>
|
||||
{% endif %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ duration|duration }}</td>
|
||||
{% if canSeeRate %}
|
||||
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||
{% for currency, rate in dayRates %}
|
||||
{{ rate|money(currency) }}
|
||||
{% if not loop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}"></td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}"></td>
|
||||
{% if canSeeUsername %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}"></td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'tags') }}"></td>
|
||||
{% for field in metaColumns %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}"></td>
|
||||
{% endfor %}
|
||||
<td class="actions"></td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
13
templates/timesheet/layout-multi-update.html.twig
Normal file
13
templates/timesheet/layout-multi-update.html.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set formEditTemplate = 'default/_form.html.twig' %}
|
||||
{% set formOptions = {
|
||||
'title': 'update_multiple'|trans({'%action%': 'action.edit'|trans, '%count%': dto.entities|length}),
|
||||
'form': form,
|
||||
} %}
|
||||
{% embed formEditTemplate with formOptions %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
7
templates/timesheet/multi-update.html.twig
Normal file
7
templates/timesheet/multi-update.html.twig
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'timesheet/layout-multi-update.html.twig' %}
|
||||
|
||||
{% block page_title %}{{ 'timesheet.title'|trans }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% import "timesheet/actions.html.twig" as actions %}
|
||||
{{ actions.timesheets_multi_update('multi-update') }}
|
||||
{% endblock %}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\TagFixtures;
|
||||
|
||||
@@ -95,4 +96,35 @@ class TagControllerTest extends ControllerBaseTest
|
||||
$editForm = $client->getCrawler()->filter('form[name=tag_edit_form]')->form();
|
||||
$this->assertEquals('Test 2 updated', $editForm->get('tag_edit_form[name]')->getValue());
|
||||
}
|
||||
|
||||
public function testMultiDeleteAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, '/admin/tags/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=multi_update_table]')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/admin/tags/multi-delete'));
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var Tag[] $tags */
|
||||
$tags = $em->getRepository(Tag::class)->findAll();
|
||||
self::assertCount(10, $tags);
|
||||
$ids = [];
|
||||
foreach ($tags as $tag) {
|
||||
$ids[] = $tag->getId();
|
||||
}
|
||||
|
||||
$client->submit($form, [
|
||||
'multi_update_table' => [
|
||||
'action' => $this->createUrl('/admin/tags/multi-delete'),
|
||||
'entities' => implode(',', $ids)
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/admin/tags/'));
|
||||
$client->followRedirect();
|
||||
|
||||
$em->clear(Tag::class);
|
||||
self::assertEquals(0, $em->getRepository(Tag::class)->count([]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,4 +324,98 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$timesheet = $em->getRepository(Timesheet::class)->find(1);
|
||||
$this->assertEquals('foo-bar', $timesheet->getDescription());
|
||||
}
|
||||
|
||||
public function testMultiDeleteAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($user);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/timesheet/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=multi_update_table]')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/timesheet/multi-delete'));
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var Timesheet[] $timesheets */
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
self::assertCount(10, $timesheets);
|
||||
$ids = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$ids[] = $timesheet->getId();
|
||||
}
|
||||
|
||||
$client->submit($form, [
|
||||
'multi_update_table' => [
|
||||
'action' => $this->createUrl('/timesheet/multi-delete'),
|
||||
'entities' => implode(',', $ids)
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||
$client->followRedirect();
|
||||
|
||||
$em->clear(Timesheet::class);
|
||||
self::assertEquals(0, $em->getRepository(Timesheet::class)->count([]));
|
||||
}
|
||||
|
||||
public function testMultiUpdate()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_SUPER_ADMIN);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($user);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/timesheet/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=multi_update_table]')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/timesheet/multi-update'));
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var Timesheet[] $timesheets */
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
self::assertCount(10, $timesheets);
|
||||
$ids = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
self::assertEmpty($timesheet->getTags());
|
||||
self::assertFalse($timesheet->isExported());
|
||||
$ids[] = $timesheet->getId();
|
||||
}
|
||||
|
||||
$client->submit($form, [
|
||||
'multi_update_table' => [
|
||||
'action' => $this->createUrl('/timesheet/multi-update'),
|
||||
'entities' => implode(',', $ids)
|
||||
]
|
||||
]);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_multi_update]')->form();
|
||||
$client->submit($form, [
|
||||
'timesheet_multi_update' => [
|
||||
'exported' => true,
|
||||
'tags' => 'test, foo-bar'
|
||||
]
|
||||
]);
|
||||
|
||||
$em->clear(Timesheet::class);
|
||||
|
||||
/** @var Timesheet[] $timesheets */
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
self::assertCount(10, $timesheets);
|
||||
foreach ($timesheets as $timesheet) {
|
||||
self::assertCount(2, $timesheet->getTags());
|
||||
self::assertTrue($timesheet->isExported());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,4 +240,102 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals('foo-bar', $timesheet->getDescription());
|
||||
$this->assertEquals($teamlead->getId(), $timesheet->getUser()->getId());
|
||||
}
|
||||
|
||||
public function testMultiDeleteAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_TEAMLEAD);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($user);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/team/timesheet/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=multi_update_table]')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/team/timesheet/multi-delete'));
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var Timesheet[] $timesheets */
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
self::assertCount(10, $timesheets);
|
||||
$ids = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$ids[] = $timesheet->getId();
|
||||
}
|
||||
|
||||
$client->submit($form, [
|
||||
'multi_update_table' => [
|
||||
'action' => $this->createUrl('/team/timesheet/multi-delete'),
|
||||
'entities' => implode(',', $ids)
|
||||
]
|
||||
]);
|
||||
$this->assertIsRedirect($client, $this->createUrl('/team/timesheet/'));
|
||||
$client->followRedirect();
|
||||
|
||||
$em->clear(Timesheet::class);
|
||||
self::assertEquals(0, $em->getRepository(Timesheet::class)->count([]));
|
||||
}
|
||||
|
||||
public function testMultiUpdate()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$user = $this->getUserByRole($em, User::ROLE_TEAMLEAD);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($user);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/team/timesheet/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=multi_update_table]')->form();
|
||||
$node = $form->getFormNode();
|
||||
$node->setAttribute('action', $this->createUrl('/team/timesheet/multi-update'));
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var Timesheet[] $timesheets */
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
self::assertCount(10, $timesheets);
|
||||
$ids = [];
|
||||
foreach ($timesheets as $timesheet) {
|
||||
self::assertFalse($timesheet->isExported());
|
||||
self::assertEquals($user->getId(), $timesheet->getUser()->getId());
|
||||
$ids[] = $timesheet->getId();
|
||||
}
|
||||
|
||||
$client->submit($form, [
|
||||
'multi_update_table' => [
|
||||
'action' => $this->createUrl('/team/timesheet/multi-update'),
|
||||
'entities' => implode(',', $ids)
|
||||
]
|
||||
]);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$newUser = $this->getUserByRole($em, User::ROLE_USER);
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_multi_update]')->form();
|
||||
$client->submit($form, [
|
||||
'timesheet_multi_update' => [
|
||||
'user' => $newUser->getId(),
|
||||
'exported' => true,
|
||||
'replaceTags' => true,
|
||||
'tags' => 'test, foo-bar, tralalala'
|
||||
]
|
||||
]);
|
||||
|
||||
$em->clear(Timesheet::class);
|
||||
|
||||
/** @var Timesheet[] $timesheets */
|
||||
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
self::assertCount(10, $timesheets);
|
||||
foreach ($timesheets as $timesheet) {
|
||||
self::assertCount(3, $timesheet->getTags());
|
||||
self::assertEquals($newUser->getId(), $timesheet->getUser()->getId());
|
||||
self::assertTrue($timesheet->isExported());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +164,9 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$this->assertHasFlashDeleteSuccess($client);
|
||||
|
||||
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
||||
// $em->clear();
|
||||
// $timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||
// $this->assertEquals(0, count($timesheets));
|
||||
// $em->clear(Timesheet::class);
|
||||
// $timesheets = $em->getRepository(Timesheet::class)->count([]);
|
||||
// $this->assertEquals(0, $timesheets);
|
||||
|
||||
$this->request($client, '/admin/user/' . $user->getId() . '/edit');
|
||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||
|
||||
51
tests/Form/MultiUpdate/MultiUpdateTableDTOTest.php
Normal file
51
tests/Form/MultiUpdate/MultiUpdateTableDTOTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Form\MultiUpdate;
|
||||
|
||||
use App\Form\MultiUpdate\MultiUpdateTableDTO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Form\MultiUpdate\MultiUpdateTableDTO
|
||||
*/
|
||||
class MultiUpdateTableDTOTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new MultiUpdateTableDTO();
|
||||
self::assertEmpty($sut->getEntities());
|
||||
self::assertEquals(['' => ''], $sut->getActions());
|
||||
self::assertNull($sut->getAction());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = new MultiUpdateTableDTO();
|
||||
|
||||
self::assertInstanceOf(MultiUpdateTableDTO::class, $sut->addUpdate('foo'));
|
||||
self::assertInstanceOf(MultiUpdateTableDTO::class, $sut->addDelete('bar'));
|
||||
self::assertInstanceOf(MultiUpdateTableDTO::class, $sut->addAction('test', 'hello/world'));
|
||||
self::assertEquals(
|
||||
[
|
||||
'' => '',
|
||||
'action.edit' => 'foo',
|
||||
'action.delete' => 'bar',
|
||||
'test' => 'hello/world'
|
||||
],
|
||||
$sut->getActions()
|
||||
);
|
||||
|
||||
self::assertInstanceOf(MultiUpdateTableDTO::class, $sut->setAction('sdfsdfsdf'));
|
||||
self::assertEquals('sdfsdfsdf', $sut->getAction());
|
||||
|
||||
self::assertInstanceOf(MultiUpdateTableDTO::class, $sut->setEntities([1, 2, 3, 4, 5, 6, 7, 8, 9, '0815']));
|
||||
self::assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, '0815'], $sut->getEntities());
|
||||
}
|
||||
}
|
||||
90
tests/Form/MultiUpdate/TimesheetMultiUpdateDTOTest.php
Normal file
90
tests/Form/MultiUpdate/TimesheetMultiUpdateDTOTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Form\MultiUpdate;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Form\MultiUpdate\TimesheetMultiUpdateDTO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Form\MultiUpdate\TimesheetMultiUpdateDTO
|
||||
*/
|
||||
class TimesheetMultiUpdateDTOTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new TimesheetMultiUpdateDTO();
|
||||
self::assertEmpty($sut->getEntities());
|
||||
self::assertEquals(['' => ''], $sut->getActions());
|
||||
self::assertNull($sut->getAction());
|
||||
|
||||
self::assertNull($sut->isExported());
|
||||
self::assertNull($sut->getProject());
|
||||
self::assertNull($sut->getAction());
|
||||
self::assertNull($sut->getCustomer());
|
||||
self::assertEquals([], $sut->getTags());
|
||||
self::assertNull($sut->getUser());
|
||||
self::assertFalse($sut->isReplaceTags());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
{
|
||||
$sut = new TimesheetMultiUpdateDTO();
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->addUpdate('foo'));
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->addDelete('bar'));
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->addAction('test', 'hello/world'));
|
||||
self::assertEquals(
|
||||
[
|
||||
'' => '',
|
||||
'action.edit' => 'foo',
|
||||
'action.delete' => 'bar',
|
||||
'test' => 'hello/world'
|
||||
],
|
||||
$sut->getActions()
|
||||
);
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setAction('sdfsdfsdf'));
|
||||
self::assertEquals('sdfsdfsdf', $sut->getAction());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setEntities([1, 2, 3, 4, 5, 6, 7, 8, 9, '0815']));
|
||||
self::assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, '0815'], $sut->getEntities());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setExported(true));
|
||||
self::assertTrue($sut->isExported());
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setExported(false));
|
||||
self::assertFalse($sut->isExported());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setTags(['foo', '0815']));
|
||||
self::assertEquals(['foo', '0815'], $sut->getTags());
|
||||
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setReplaceTags(true));
|
||||
self::assertTrue($sut->isReplaceTags());
|
||||
|
||||
$user = (new User())->setUsername('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setUser($user));
|
||||
self::assertSame($user, $sut->getUser());
|
||||
|
||||
$activity = (new Activity())->setName('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setActivity($activity));
|
||||
self::assertSame($activity, $sut->getActivity());
|
||||
|
||||
$project = (new Project())->setName('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setProject($project));
|
||||
self::assertSame($project, $sut->getProject());
|
||||
|
||||
$customer = (new Customer())->setName('sdfsdfsd');
|
||||
self::assertInstanceOf(TimesheetMultiUpdateDTO::class, $sut->setCustomer($customer));
|
||||
self::assertSame($customer, $sut->getCustomer());
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,6 @@ class UserQueryTest extends BaseQueryTest
|
||||
protected function assertRole(UserQuery $sut)
|
||||
{
|
||||
$this->assertNull($sut->getRole());
|
||||
|
||||
$sut->setRole('foo-bar');
|
||||
$this->assertNull($sut->getRole());
|
||||
|
||||
$sut->setRole('ROLE_USER');
|
||||
$this->assertEquals('ROLE_USER', $sut->getRole());
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
<source>my.profile</source>
|
||||
<target>Mein Profil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="update_multiple">
|
||||
<source>update_multiple</source>
|
||||
<target>%action% von %count% Einträgen?</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Login / Security
|
||||
@@ -404,6 +408,14 @@
|
||||
<source>label.color</source>
|
||||
<target>Farbe</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.replaceTags">
|
||||
<source>label.replaceTags</source>
|
||||
<target>Schlagworte ersetzen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.appendTags">
|
||||
<source>label.appendTags</source>
|
||||
<target>Schlagworte hinzufügen</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
User profile
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
<source>my.profile</source>
|
||||
<target>My profile</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="update_multiple">
|
||||
<source>update_multiple</source>
|
||||
<target>%action% %count% entries?</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Login / Security
|
||||
@@ -404,6 +408,14 @@
|
||||
<source>label.color</source>
|
||||
<target>Color</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.replaceTags">
|
||||
<source>label.replaceTags</source>
|
||||
<target>Replace tags</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.appendTags">
|
||||
<source>label.appendTags</source>
|
||||
<target>Append tags</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
User profile
|
||||
|
||||
@@ -6915,10 +6915,10 @@ svgo@^1.0.0:
|
||||
unquote "~1.1.1"
|
||||
util.promisify "~1.0.0"
|
||||
|
||||
sweetalert2@^8.11.7:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-8.14.1.tgz#591bb63056198f28c7118dde105ba4ae7a24e612"
|
||||
integrity sha512-5kL0iJYG2LP62xPqPuwDwpDWeTKD4AEvV7e5OM2q794F3ko/yFyPHzOGjnRGa3SqkS8U9syt5OvZyLZ27nGMog==
|
||||
sweetalert2@^9.3:
|
||||
version "9.3.4"
|
||||
resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-9.3.4.tgz#2fc1fcd8538ea6613cbc80bc49e6d13f1b0150f6"
|
||||
integrity sha512-nyW+SAZUkiKpbKSDE33J9nF5UQnc7IGC1eDHeW80HkhsggvkmQN0Clq7PHXb1roLkBLJryb2evVLDc9jZmptRQ==
|
||||
|
||||
syntax-error@^1.1.1:
|
||||
version "1.4.0"
|
||||
|
||||
Reference in New Issue
Block a user