improved play and stop button (#2692)

* improved play and stop button if only one record is allowed
* stop button first in action menu
* fix skin colors
* autofocus term field in search form
* fix responsive column layout in invoice listing
* improve responsive header for small screens
This commit is contained in:
Kevin Papst
2021-07-30 19:57:57 +02:00
committed by GitHub
parent 37e02365c7
commit df62c8a428
19 changed files with 353 additions and 245 deletions

View File

@@ -24,18 +24,14 @@ export default class KimaiActiveRecords extends KimaiPlugin {
} }
init() { init() {
const menu = document.querySelector(this.selector); this.menu = document.querySelector(this.selector);
// the menu can be hidden if user has no permissions to see it // the menu can be hidden if user has no permissions to see it
if (menu === null) { if (this.menu === null) {
return; return;
} }
const dropdown = menu.querySelector('ul.dropdown-menu'); this.attributes = this.menu.dataset;
this.attributes = dropdown.dataset;
this.itemList = dropdown.querySelector('li > ul.menu');
this.label = menu.querySelector('a > span.label');
const self = this; const self = this;
const handle = function() { self.reloadActiveRecords(); }; const handle = function() { self.reloadActiveRecords(); };
@@ -50,15 +46,10 @@ export default class KimaiActiveRecords extends KimaiPlugin {
document.addEventListener('kimai.customerDelete', handle); document.addEventListener('kimai.customerDelete', handle);
} }
emptyList() {
this.itemList.innerHTML = '';
}
_toggleMenu(hasEntries) { _toggleMenu(hasEntries) {
const menu = document.querySelector(this.selector); this.menu.style.display = hasEntries ? 'inline-block' : 'none';
const menuEmpty = document.querySelector(this.selectorEmpty);
menu.style.display = hasEntries ? 'inline-block' : 'none'; const menuEmpty = document.querySelector(this.selectorEmpty);
if (menuEmpty !== null) { if (menuEmpty !== null) {
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none'; menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
} }
@@ -67,42 +58,52 @@ export default class KimaiActiveRecords extends KimaiPlugin {
setEntries(entries) { setEntries(entries) {
this._toggleMenu(entries.length > 0); this._toggleMenu(entries.length > 0);
const template = this.menu.querySelector('[data-template="active-record"]');
const label = this.menu.querySelector('a > span.label');
if (label !== null) {
label.innerText = entries.length === 0 ? '' : entries.length;
}
if (entries.length === 0) { if (entries.length === 0) {
this.label.innerText = '';
this.emptyList();
return; return;
} }
let htmlToInsert = ''; if (template === null) {
const durations = this.getContainer().getPlugin('timesheet-duration'); this._replaceInNode(this.menu, entries[0]);
for (let timesheet of entries) {
htmlToInsert +=
`<li>` +
`<a href="${ this.attributes['href'].replace('000', timesheet.id) }" data-event="kimai.timesheetStop kimai.timesheetUpdate" class="api-link" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">` +
`<div class="pull-left">` +
`<i class="${ this.attributes['icon'] } fa-2x"></i>` +
`</div>` +
`<h4>` +
`<span>${ timesheet.activity.name }</span>` +
`<small>` +
`<span data-title="true" data-since="${ timesheet.begin }">${ durations.formatDuration(timesheet.duration) }</span>` +
`</small>` +
`</h4>` +
`<p>${ timesheet.project.name } (${ timesheet.project.customer.name })</p>` +
`</a>` +
`</li>`;
}
if (this.label.dataset.warning < entries.length) {
this.label.classList = 'label label-danger';
} else { } else {
this.label.classList = 'label label-warning'; const container = template.parentElement;
} container.innerHTML = '';
this.label.innerText = entries.length;
this.itemList.innerHTML = htmlToInsert;
durations.updateRecords(); for (let timesheet of entries) {
const newNode = template.cloneNode(true);
container.appendChild(this._replaceInNode(newNode, timesheet));
}
}
this.getContainer().getPlugin('timesheet-duration').updateRecords();
}
_replaceInNode(node, timesheet) {
const date = this.getContainer().getPlugin('date');
const allReplacer = node.querySelectorAll('[data-replacer]');
for (let node of allReplacer) {
const replacerName = node.dataset['replacer'];
if (replacerName === 'url') {
node.href = this.attributes['href'].replace('000', timesheet.id);
} else if (replacerName === 'activity') {
node.innerText = timesheet.activity.name;
} else if (replacerName === 'project') {
node.innerText = timesheet.project.name;
} else if (replacerName === 'customer') {
node.innerText = timesheet.project.customer.name;
} else if (replacerName === 'duration') {
node.dataset['since'] = timesheet.begin;
node.innerText = date.formatDuration(timesheet.duration);
}
}
return node;
} }
reloadActiveRecords() { reloadActiveRecords() {

View File

@@ -31,36 +31,28 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
this.isDirty = false; this.isDirty = false;
this.modal = jQuery('#remote_form_modal'); this.modal = jQuery('#remote_form_modal');
this.modal.on('hide.bs.modal', function (e) { this.modal
if (self.isDirty) { .on('hide.bs.modal', function (e) {
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) { if (self.isDirty) {
const msg = self.getContainer().getTranslation().get('modal.dirty'); if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>'); const msg = self.getContainer().getTranslation().get('modal.dirty');
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
}
e.preventDefault();
return;
} }
e.preventDefault(); jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
return; self.isDirty = false;
} self.getContainer().getPlugin('event').trigger('modal-hide');
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler); })
self.isDirty = false; .on('hidden.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-hide'); // kill all references, so GC can kick in
}); self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier());
this.modal.on('hidden.bs.modal', function () { jQuery('#remote_form_modal .modal-body').replaceWith('');
// kill all references, so GC can kick in })
self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier()); .on('show.bs.modal', function () {
jQuery('#remote_form_modal .modal-body').replaceWith(''); self.getContainer().getPlugin('event').trigger('modal-show');
}); });
this.modal.on('show.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-show');
});
this.modal.on('shown.bs.modal', function () {
// workaround for autofocus attribute, as the modal "steals" it
let formAutofocus = jQuery(self._getFormIdentifier()).find('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select');
}
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
this._addClickHandler(this.selector, function(href) { this._addClickHandler(this.selector, function(href) {
self.openUrlInModal(href); self.openUrlInModal(href);

View File

@@ -24,6 +24,30 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
// activate all form plugins // activate all form plugins
this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body'); this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body');
this.getContainer().getPlugin('form').activateForm('form.searchform', 'body'); this.getContainer().getPlugin('form').activateForm('form.searchform', 'body');
this.registerModalAutofocus('#modal_search');
this.registerModalAutofocus('#remote_form_modal');
}
/**
* workaround for autofocus attribute, as the modal "steals" it
*
* @param {string} selector
*/
registerModalAutofocus(selector) {
let modal = jQuery(selector);
if (modal.length === 0) {
return;
}
modal.on('shown.bs.modal', function () {
let form = modal.find('form');
let formAutofocus = form.find('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = form.find('input[type=text],textarea,select');
}
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
} }
/** /**

View File

@@ -15,43 +15,21 @@ $avatar-base-size: 30;
line-height: 1; line-height: 1;
position: relative; position: relative;
font-weight: bold; font-weight: bold;
font-size: ($avatar-base-size / 2 - 2)+px; font-size: ceil($avatar-base-size / 2 - 2)+px;
top: ($avatar-base-size / 4 - 1)+px; top: ceil($avatar-base-size / 4 - 1)+px;
} }
.avatar-xs { $avatarSizes: "xs" .75, "sm" 1.25, "md" 1.5, "lg" 2;
width: ($avatar-base-size * 0.75)+px;
height: ($avatar-base-size * 0.75)+px;
.initials {
font-size: ($avatar-base-size * .75 / 2 - 2)+px;
top: ($avatar-base-size * .75 / 4 - 1)+px;
}
}
.avatar-sm { @each $avatarSizeName, $avatarBaseSize in $avatarSizes {
width: ($avatar-base-size * 1.25)+px; .avatar-#{$avatarSizeName} {
height: ($avatar-base-size * 1.25)+px; width: ceil($avatar-base-size * $avatarBaseSize)+px;
.initials { height: ceil($avatar-base-size * $avatarBaseSize)+px;
font-size: ($avatar-base-size * 1.25 / 2 - 2)+px;
top: ($avatar-base-size * 1.25 / 4 - 1)+px;
}
}
.avatar-md { .initials {
width: ($avatar-base-size * 1.5)+px; font-size: ceil($avatar-base-size * $avatarBaseSize / 2 - 2)+px;
height: ($avatar-base-size * 1.5)+px; top: ceil($avatar-base-size * $avatarBaseSize / 4 - 1)+px;
.initials { }
font-size: ($avatar-base-size * 1.5 / 2 - 2)+px;
top: ($avatar-base-size * 1.5 / 4 - 1)+px;
}
}
.avatar-lg {
width: ($avatar-base-size * 2)+px;
height: ($avatar-base-size * 2)+px;
.initials {
font-size: ($avatar-base-size * 2 / 2 - 2)+px;
top: ($avatar-base-size * 2 / 4 - 1)+px;
} }
} }
@@ -59,11 +37,11 @@ $avatar-base-size: 30;
border-radius: 50%; border-radius: 50%;
border: 3px solid #fff; border: 3px solid #fff;
.avatar { .avatar {
width: ($avatar-base-size * 2.75)+px; width: ceil($avatar-base-size * 2.75)+px;
height: ($avatar-base-size * 2.75)+px; height: ceil($avatar-base-size * 2.75)+px;
.initials { .initials {
font-size: ($avatar-base-size * 2.75 / 2 - 2)+px; font-size: ceil($avatar-base-size * 2.75 / 2 - 2)+px;
top: ($avatar-base-size * 2.75 / 4 - 1)+px; top: ceil($avatar-base-size * 2.75 / 4 - 1)+px;
} }
} }
} }

View File

@@ -32,6 +32,9 @@
} }
@media (max-width: $screen-md-min) { @media (max-width: $screen-md-min) {
.content-header h1 {
font-size: 1.5em;
}
.content-header>.breadcrumb { .content-header>.breadcrumb {
margin-top: 0; margin-top: 0;
} }

View File

@@ -5,22 +5,34 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
.navbar-custom-menu {
.navbar-nav {
li>.ddt-small {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
.navbar-nav { .navbar-nav {
img.img-circle { img.img-circle {
max-height: 26px; max-height: 26px;
margin-top: -2px; margin-top: -2px;
} }
.avatar { .avatar {
max-height: 26px; max-height: 30px;
max-width: 26px; max-width: 30px;
margin-top: -2px; height: 30px;
width: 30px;
margin-top: 0;
.initials { .initials {
top: 4px; top: 5px;
left: 0;
} }
} }
li>a.ddt-large { li>.ddt-small {
padding: 15px 12px 11px 10px; padding-top: 10px;
font-size: 10px; padding-bottom: 10px;
} }
li.messages-menu ul.menu li { li.messages-menu ul.menu li {
a:hover .pull-left i { a:hover .pull-left i {
@@ -43,17 +55,13 @@
width: unset !important; width: unset !important;
} }
.user-menu { .user-menu {
margin-right: 10px;
ul { ul {
li { li {
a { a {
color: #444;
&:hover { &:hover {
background-color: #f4f4f4; background-color: #f4f4f4;
i {
color: $blue;
}
} }
color: #444;
i { i {
padding-right: 10px; padding-right: 10px;
} }
@@ -64,9 +72,6 @@
} }
} }
} }
.notifications-menu>.dropdown-menu>li .menu>li>a:hover i {
color: $green;
}
} }
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
@@ -80,25 +85,69 @@
padding: 5px 25px 5px 25px padding: 5px 25px 5px 25px
} }
} }
@keyframes ticktac-blink {
/* 0% { opacity: 1; }
.ticktac:hover i.running{ 5% { opacity: 0.95; }
color: #4ff131; 10% { opacity: 0.9; }
15% { opacity: 0.85; }
20% { opacity: 0.8; }
25% { opacity: 0.75; }
30% { opacity: 0.7; }
35% { opacity: 0.65; }
40% { opacity: 0.6; }
45% { opacity: 0.65; }
50% { opacity: 0.7; }
55% { opacity: 0.75; }
60% { opacity: 0.8; }
65% { opacity: 0.85; }
70% { opacity: 0.9; }
75% { opacity: 0.95; }
100% { opacity: 1; }
}
.ticktac-single {
display: block;
position: relative;
padding-left: 15px;
padding-right: 15px;
i {
animation: ticktac-blink 2s step-end infinite;
}
span {
font-size: 1.8em;
line-height: 1em;
padding-left: 5px;
padding-right: 0;
}
&:hover {
background: rgba(0,0,0,.3);
}
}
body.fixed {
.navbar-nav {
.user-menu {
margin-right: 10px;
}
}
} }
li.open .ticktac i.running{ @media (max-width: $screen-sm-min) {
color: #4ff131; .ticktac-single {
padding-left: 5px;
padding-right: 5px;
}
.navbar-custom-menu .navbar-nav li>.ddt-small {
padding-left: 5px;
padding-right: 5px;
}
} }
.ticktac i.stopped{ @media (max-width: $screen-md-min) {
color: #fff; .ticktac-single {
padding-left: 10px;
padding-right: 10px;
}
.navbar-custom-menu .navbar-nav li>.ddt-small {
padding-left: 10px;
padding-right: 10px;
}
} }
.ticktac:hover i.stopped{
color: #4ff131;
}
.user-panel>.info {
left: 40px;
padding-top: 0px;
}
*/

View File

@@ -1,26 +1,69 @@
$skins: blue, blue, black, green, purple, red, yellow; $skins:
"blue" $yellow #fff $yellow $red,
"black" $yellow #000 $green $red,
"green" $yellow #fff $yellow $red,
"purple" $yellow #fff $yellow $red,
"red" $yellow #fff $yellow $yellow,
"yellow" $blue #fff $green $red
;
@each $skinColor in $skins { @each $skinColor, $navHighlight, $tickTacColor, $startButton, $stopButton in $skins {
@media (max-width: $screen-sm-min) { @each $prefix in (".skin-#{$skinColor}", ".skin-#{$skinColor}-light") {
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li.divider { @media (max-width: $screen-sm-min) {
background-color: #ccc; #{$prefix} {
} .main-header .navbar .dropdown-menu li.divider {
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a { background-color: #ccc;
color: #444; }
}
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a:hover {
background: #fff;
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li.divider { .main-header .navbar .dropdown-menu li a {
background-color: #ccc; color: #444;
}
.main-header .navbar .dropdown-menu li a:hover {
background: #fff;
}
}
} }
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a { #{$prefix} {
color: #444; .navbar-nav {
} .user-menu {
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a:hover { ul {
background: #fff; li {
a {
&:hover {
i {
color: $navHighlight;
}
}
}
}
}
}
.notifications-menu > .dropdown-menu > li .menu > li > a:hover i {
color: $navHighlight;
}
}
.ticktac-single {
i {
color: $tickTacColor;
}
span {
color: $tickTacColor;
}
&:hover {
.ticktac-stop {
i {
color: $stopButton;
}
}
.ticktac-start {
i {
color: $startButton;
}
}
}
}
} }
} }
} }

View File

@@ -25,7 +25,7 @@ $green: #00a65a;
// Info // Info
//$aqua: #00c0ef; //$aqua: #00c0ef;
// Warning // Warning
//$yellow: #f39c12; $yellow: #f0ad4e;
$blue: #0073b7; $blue: #0073b7;
//$navy: #001F3F; //$navy: #001F3F;
//$teal: #39CCCC; //$teal: #39CCCC;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,10 @@
"app": { "app": {
"js": [ "js": [
"build/runtime.b8e7bb04.js", "build/runtime.b8e7bb04.js",
"build/app.07c264a7.js" "build/app.65f97645.js"
], ],
"css": [ "css": [
"build/app.7aa10842.css" "build/app.beac1734.css"
] ]
}, },
"invoice": { "invoice": {

View File

@@ -1,6 +1,6 @@
{ {
"build/app.css": "build/app.7aa10842.css", "build/app.css": "build/app.beac1734.css",
"build/app.js": "build/app.07c264a7.js", "build/app.js": "build/app.65f97645.js",
"build/invoice.css": "build/invoice.ff32661a.css", "build/invoice.css": "build/invoice.ff32661a.css",
"build/invoice.js": "build/invoice.19f36eca.js", "build/invoice.js": "build/invoice.19f36eca.js",
"build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css", "build/invoice-pdf.css": "build/invoice-pdf.9a7468ef.css",

View File

@@ -24,11 +24,6 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
/** @var Timesheet $timesheet */ /** @var Timesheet $timesheet */
$timesheet = $payload['timesheet']; $timesheet = $payload['timesheet'];
if ($timesheet->getId() !== null) { if ($timesheet->getId() !== null) {
if ($this->isGranted('edit', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
}
if ($timesheet->isRunning() && $this->isGranted('stop', $timesheet)) { if ($timesheet->isRunning() && $this->isGranted('stop', $timesheet)) {
$event->addAction('stop', ['url' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]); $event->addAction('stop', ['url' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]);
} }
@@ -37,6 +32,11 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
$event->addAction('repeat', ['url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]); $event->addAction('repeat', ['url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
} }
if ($this->isGranted('edit', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
}
if ($this->isGranted('duplicate', $timesheet)) { if ($this->isGranted('duplicate', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form'; $class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]); $event->addAction('copy', ['url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class]);

View File

@@ -88,82 +88,100 @@
<!-- Page rendered on {{ 'now'|date_full }} --> <!-- Page rendered on {{ 'now'|date_full }} -->
{% endblock %} {% endblock %}
{% block navbar_start %} {#
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %} {% block navbar_toggle %}
{% if is_granted('view_own_timesheet') %} <a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button">
<span class="sr-only">{{ 'Toggle navigation'|trans({}, 'AdminLTEBundle') }}</span>
</a>
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') and is_granted('view_own_timesheet') %}
<div class="navbar-custom-menu" style="float:left">
<ul class="nav navbar-nav">
{% if app.user.preferenceValue('login.initial_view') == 'calendar' %} {% if app.user.preferenceValue('login.initial_view') == 'calendar' %}
<li class="visible-xs-inline-block"> <li class="pull-left visible-xs-inline-block">
<a href="{{ path('calendar') }}" class="ddt-large"> <a href="{{ path('calendar') }}" class="ddt-small">
<i class="{{ 'calendar'|icon }} fa-2x"></i> <i class="{{ 'calendar'|icon }} fa-2x"></i>
</a> </a>
</li> </li>
{% else %} {% else %}
<li class="visible-xs-inline-block"> <li class="pull-left visible-xs-inline-block">
<a href="{{ path('timesheet') }}" class="ddt-large"> <a href="{{ path('timesheet') }}" class="ddt-small">
<i class="{{ 'timesheet'|icon }} fa-2x"></i> <i class="{{ 'timesheet'|icon }} fa-2x"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% endif %}
{% if is_granted('view_invoice') %}
<li class="visible-xs-inline-block">
<a href="{{ path('invoice') }}" class="ddt-large">
<i class="{{ 'invoice'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
{% if is_granted('create_export') %}
<li class="visible-xs-inline-block">
<a href="{{ path('export') }}" class="ddt-large">
<i class="{{ 'export'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
{% block navbar_extensions %}{% endblock %}
{% set active_timesheets = active_timesheets(app.user) %}
{% set active_limit = kimai_config.timesheetActiveEntriesHardLimit %}
<li class="dropdown messages-menu" style="{% if active_timesheets is empty %}display:none{% endif %}">
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span data-warning="{{ active_limit }}" class="label label-{% if active_timesheets|length > active_limit %}danger{% else %}warning{% endif %}">{% if active_timesheets|length > 0 %}{{ active_timesheets|length }}{% endif %}</span>
</a>
<ul class="dropdown-menu"
data-api="{{ path('active_timesheet') }}"
data-href="{{ path('stop_timesheet', {'id' : '000'}) }}"
data-icon="{{ 'stop-small'|icon }}">
<li class="header">
{{ 'active.entries'|trans }}
</li>
<li>
<ul class="menu">
{% for entry in active_timesheets %}
<li>
<a href="{{ path('stop_timesheet', {'id' : entry.id}) }}" class="api-link" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<div class="pull-left">
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
</div>
<h4>
<span>{{ entry.activity.name }}</span>
<small>
<span data-title="true" data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</small>
</h4>
<p>{{ entry.project.name }} ({{ entry.project.customer.name }})</p>
</a>
</li>
{% endfor %}
</ul>
</li>
{% if is_granted('create_own_timesheet') %}
<li class="footer"><a href="{{ path('timesheet_create') }}" class="modal-ajax-form">{{ 'timesheet.start'|trans }}</a></li>
{% endif %}
</ul> </ul>
</li> </div>
{% endif %}
{% endblock %}
#}
{% block navbar_start %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{% block navbar_extensions %}{% endblock %}
{% if is_granted('create_own_timesheet') %} {% if is_granted('create_own_timesheet') %}
<li class="messages-menu-empty" style="{% if active_timesheets is not empty %}display:none{% endif %}"> {% set active_timesheets = active_timesheets(app.user) %}
<a href="{{ path('timesheet_create') }}" class="ddt-large modal-ajax-form"> {% set hasActiveRecords = active_timesheets is not empty %}
<i class="{{ 'start'|icon }} fa-2x"></i> {% if not hasActiveRecords %}
</a> {# fake entry, because at least one html template node is needed #}
{% set active_timesheets = [{'id': '000', 'begin': create_date('now'), 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
{% endif %}
{% set active_limit = kimai_config.timesheetActiveEntriesHardLimit %}
<li class="messages-menu {% if active_limit > 1 and hasActiveRecords %}dropdown{% endif %}" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" data-icon="{{ 'stop-small'|icon }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
{% if active_limit == 1 %}
{% set entry = active_timesheets[0] %}
<div class="ddt-small ticktac-single ticktac-running">
<div class="ticktac-stop">
<a data-replacer="url" class="api-link" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<i class="{{ 'stop-small'|icon }} fa-2x"></i><span data-replacer="duration" data-title="true" data-since="{{ entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</a>
</div>
</div>
{% else %}
<a href="#" class="dropdown-toggle ddt-small ticktac" data-toggle="dropdown">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span data-warning="{{ active_limit }}" class="label label-warning">{% if hasActiveRecords %}{{ active_timesheets|length }}{% endif %}</span>
</a>
<ul class="dropdown-menu">
<li class="header">
{{ 'active.entries'|trans }}
</li>
<li>
<ul class="menu">
{% for entry in active_timesheets %}
<li data-template="active-record">
<a data-replacer="url" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" class="api-link" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<div class="pull-left">
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
</div>
<h4>
<span data-replacer="activity">{{ entry.activity.name }}</span>
<small>
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</small>
</h4>
<p><span data-replacer="project">{{ entry.project.name }}</span> (<span data-replacer="customer">{{ entry.project.customer.name }}</span>)</p>
</a>
</li>
{% endfor %}
</ul>
</li>
<li class="footer"><a href="{{ path('timesheet_create') }}" class="modal-ajax-form">{{ 'timesheet.start'|trans }}</a></li>
</ul>
{% endif %}
</li>
<li class="messages-menu-empty" style="{% if hasActiveRecords %}display:none{% endif %}">
{% if active_limit == 1 %}
<div class="ddt-small ticktac-single ticktac-stopped">
<a href="{{ path('timesheet_create') }}" class="modal-ajax-form ticktac-start">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span>{{ 0|duration }}</span>
</a>
</div>
{% else %}
<a href="{{ path('timesheet_create') }}" class="ddt-small ticktac-start modal-ajax-form">
<i class="{{ 'start'|icon }} fa-2x"></i>
</a>
{% endif %}
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -188,7 +206,7 @@
{% endif %} {% endif %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
<li class="dropdown user-menu"> <li class="dropdown user-menu">
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown"> <a href="#" class="dropdown-toggle ddt-small" data-toggle="dropdown">
{{ widgets.user_avatar(app.user, false) }} {{ widgets.user_avatar(app.user, false) }}
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">

View File

@@ -6,7 +6,7 @@
{% import "invoice/macros.html.twig" as macros %} {% import "invoice/macros.html.twig" as macros %}
{% set columns = { {% set columns = {
'date': {'class': 'alwaysVisible w-min', 'orderBy': false}, 'date': {'class': 'alwaysVisible', 'orderBy': false},
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false}, 'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false}, 'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
'invoice_number': {'class': 'hidden-xs hidden-sm text-center w-min', 'title': 'invoice.number'|trans, 'orderBy': false}, 'invoice_number': {'class': 'hidden-xs hidden-sm text-center w-min', 'title': 'invoice.number'|trans, 'orderBy': false},

View File

@@ -1,7 +1,7 @@
{% if is_granted('start_own_timesheet') %} {% if is_granted('start_own_timesheet') %}
<li class="dropdown notifications-menu"> <li class="dropdown notifications-menu">
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown"> <a href="#" class="dropdown-toggle ddt-small" data-toggle="dropdown">
<i class="{{ 'activity'|icon }} fa-2x"></i> <i class="{{ 'repeat'|icon }} fa-2x"></i>
</a> </a>
<ul class="dropdown-menu" <ul class="dropdown-menu"
data-api="{{ path('recent_timesheet', {'size' : 10}) }}" data-api="{{ path('recent_timesheet', {'size' : 10}) }}"

View File

@@ -82,8 +82,8 @@ class LayoutControllerTest extends ControllerBaseTest
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
self::assertStringContainsString('<li class="dropdown messages-menu" style="display:none">', $content); self::assertStringContainsString('<li class="messages-menu', $content);
self::assertStringContainsString('<ul class="dropdown-menu"', $content); self::assertStringContainsString('<div class="ddt-small ticktac-single ', $content);
self::assertStringContainsString('data-api="', $content); self::assertStringContainsString('data-api="', $content);
self::assertStringContainsString('data-href="', $content); self::assertStringContainsString('data-href="', $content);
self::assertStringContainsString('data-icon=', $content); self::assertStringContainsString('data-icon=', $content);