Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -1,20 +1,13 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% block page_title %}{{ 'calendar'|trans }}{% endblock %}
|
||||
{% block page_actions %}
|
||||
{% set event = actions(app.user, 'calendar', 'index') %}
|
||||
{{ widgets.page_actions(event.actions) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row">
|
||||
{% set hasTwoColumns = (config.dragDropAmount > 0 and dragAndDrop is not empty and (dragAndDrop|filter(s => s.entries|length > 0)|length > 0)) or form is not null %}
|
||||
{% if hasTwoColumns %}
|
||||
<div class="hidden-xs col-sm-5 col-md-4 col-lg-3 no-print">
|
||||
<div class="col-md-4 col-lg-3 d-none d-md-block">
|
||||
{% if form is not null %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% block box_body %}
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
@@ -23,18 +16,22 @@
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
{% for source in dragAndDrop|filter(s => s.entries|length > 0) %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% block box_title %}{{ source.title|trans }}{% endblock %}
|
||||
{% block box_body_class %}drag-and-drop-source{% endblock %}
|
||||
{% block box_body_class %}drag-and-drop-source p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
<div class="external-events" data-method="{{ source.method }}" data-route="{{ path(source.route, source.routeParams) }}" data-route-replacer="{{ source.routeReplacer|json_encode|e('html_attr') }}">
|
||||
<div class="list-group list-group-flush external-events" data-method="{{ source.method }}" data-route="{{ path(source.route, source.routeParams) }}" data-route-replacer="{{ source.routeReplacer|json_encode|e('html_attr') }}">
|
||||
{% for entry in source.entries|slice(0, config.dragDropAmount) %}
|
||||
<div draggable="true" style="background-color: {{ entry.color }}; color: {{ entry.color|font_contrast }}" class="external-event ui-draggable ui-draggable-handle" data-entry="{{ entry.data|json_encode|e('html_attr') }}"{% if entry.project is not null %} data-toggle="tooltip" title="{{ entry.project.customer.name }}"{% endif %}>
|
||||
{% if source.blockInclude is not null and entry.blockName is not null and block(entry.blockName, source.blockInclude) is defined %}
|
||||
{{ block(entry.blockName, source.blockInclude) }}
|
||||
{% else %}
|
||||
<span class="title">{{ entry.title }}</span>
|
||||
{% endif %}
|
||||
<div class="p-1 ps-2 list-group-item external-event draggable" data-entry="{{ entry.data|json_encode|e('html_attr') }}"{% if entry.project is not null %} data-toggle="tooltip" title="{{ entry.project.customer.name }}"{% endif %}>
|
||||
<div class="row align-items-center">
|
||||
<div class="col text-truncate">
|
||||
{% if source.blockInclude is not null and entry.blockName is not null and block(entry.blockName, source.blockInclude) is defined %}
|
||||
{{ block(entry.blockName, source.blockInclude) }}
|
||||
{% else %}
|
||||
<span class="d-block text-muted text-truncate mt-n1">{{ entry.title }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -44,9 +41,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-xs-12 {% if hasTwoColumns %}col-sm-7 col-md-8 col-lg-9 col-print-12{% endif %}">
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_body_class %}no-padding{% endblock %}
|
||||
<div class="col-xs-12 {% if hasTwoColumns %}col-md-8 col-lg-9 col-print-12{% endif %}">
|
||||
{% embed '@theme/embeds/card.html.twig' %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
<div id="timesheet_calendar"></div>
|
||||
{% endblock %}
|
||||
@@ -66,481 +63,140 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% set calendarSelector = '#timesheet_calendar' %}
|
||||
{% set calendarSelector = 'timesheet_calendar' %}
|
||||
{% set createParams = '' %}
|
||||
{% set createRoute = 'timesheet_create' %}
|
||||
{% set editRoute = 'timesheet_edit' %}
|
||||
{% set canDelete = is_granted('delete_own_timesheet') %}
|
||||
{% set canCreate = is_granted('create_own_timesheet') %}
|
||||
{% set canEdit = is_granted('edit_own_timesheet') %}
|
||||
{% set isForeignEdit = false %}
|
||||
{% if user != app.user %}
|
||||
{% set isForeignEdit = true %}
|
||||
{% set createParams = '&user=' ~ user.id %}
|
||||
{% set createParams = 'user=' ~ user.id ~ '&' %}
|
||||
{% set createRoute = 'admin_timesheet_create' %}
|
||||
{% set editRoute = 'admin_timesheet_edit' %}
|
||||
{% set canDelete = is_granted('delete_other_timesheet') %}
|
||||
{% set canCreate = is_granted('create_other_timesheet') %}
|
||||
{% set canEdit = is_granted('edit_other_timesheet') %}
|
||||
{% endif %}
|
||||
|
||||
{{ parent() }}
|
||||
<script>
|
||||
activatePopover();
|
||||
{# using var, so it can be re-used in the different scopes #}
|
||||
var calendar = null;
|
||||
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
|
||||
});
|
||||
|
||||
document.addEventListener('kimai.timesheetUpdate', function() {
|
||||
jQuery('{{ calendarSelector }}').fullCalendar('refetchEvents');
|
||||
});
|
||||
|
||||
{# https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/ #}
|
||||
function calculateFontContrastColor(hexcolor)
|
||||
{
|
||||
if (hexcolor.slice(0, 1) === '#') {
|
||||
hexcolor = hexcolor.slice(1);
|
||||
}
|
||||
|
||||
if (hexcolor.length === 3) {
|
||||
hexcolor = hexcolor.split('').map(function (hex) { return hex + hex; }).join('');
|
||||
}
|
||||
|
||||
let r = parseInt(hexcolor.substr(0,2),16);
|
||||
let g = parseInt(hexcolor.substr(2,2),16);
|
||||
let b = parseInt(hexcolor.substr(4,2),16);
|
||||
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||
|
||||
return (yiq >= 128) ? '#000000' : '#ffffff';
|
||||
}
|
||||
|
||||
function convertApiTimesheetToCalendar(apiItem)
|
||||
{
|
||||
const defaultColor = kimai.getConfiguration().get('defaultColor');
|
||||
let color = apiItem.activity.color;
|
||||
if (color === null || color === defaultColor) {
|
||||
color = apiItem.project.color;
|
||||
if (color === null || color === defaultColor) {
|
||||
color = apiItem.project.customer.color;
|
||||
}
|
||||
}
|
||||
if (color == null) {
|
||||
color = defaultColor;
|
||||
}
|
||||
|
||||
let title = apiItem.activity.name;
|
||||
|
||||
let description = '';
|
||||
if (apiItem.description !== null && apiItem.description !== '') {
|
||||
description = '{{ constant('\\App\\Form\\Type\\CalendarTitlePatternType::SPACER') }}' + apiItem.description;
|
||||
}
|
||||
|
||||
{% if config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT') %}
|
||||
title = apiItem.project.name;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_CUSTOMER') %}
|
||||
title = apiItem.project.customer.name;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_DESCRIPTION') %}
|
||||
title = apiItem.description;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_ACTIVITY_DESCRIPTION') %}
|
||||
title = apiItem.activity.name + description;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION') %}
|
||||
title = apiItem.project.name + description;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION') %}
|
||||
title = apiItem.project.customer.name + description;
|
||||
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT_CUSTOMER') %}
|
||||
title = apiItem.project.name + apiItem.project.customer.name;
|
||||
{% endif %}
|
||||
|
||||
if (title === '' || title === null) {
|
||||
title = apiItem.activity.name;
|
||||
}
|
||||
|
||||
return {
|
||||
id: apiItem.id,
|
||||
title: title,
|
||||
description: apiItem.description,
|
||||
start: apiItem.begin,
|
||||
end: apiItem.end,
|
||||
activity: apiItem.activity.name,
|
||||
project: apiItem.project.name,
|
||||
customer: apiItem.project.customer.name,
|
||||
tags: apiItem.tags,
|
||||
color: color,
|
||||
textColor: calculateFontContrastColor(color),
|
||||
};
|
||||
}
|
||||
|
||||
function renderEventPopoverTitle(eventObj)
|
||||
{
|
||||
return eventObj.start.format('L') + ' | ' + eventObj.start.format('LT') + ' - ' + (eventObj.end ? eventObj.end.format('LT') : '');
|
||||
}
|
||||
|
||||
function renderEventPopoverContent(eventObj)
|
||||
{
|
||||
const escaper = kimai.getPlugin('escape');
|
||||
|
||||
return '<div class="calendar-entry">' +
|
||||
'<ul>' +
|
||||
'<li>' + '{{ 'label.customer'|trans }}: ' + escaper.escapeForHtml(eventObj.customer) + '</li>' +
|
||||
'<li>' + '{{ 'label.project'|trans }}: ' + escaper.escapeForHtml(eventObj.project) + '</li>' +
|
||||
'<li>' + '{{ 'label.activity'|trans }}: ' + escaper.escapeForHtml(eventObj.activity) + '</li>' +
|
||||
'</ul>' +
|
||||
(eventObj.description !== null || eventObj.tags.length > 0 ? '<hr>' : '') +
|
||||
(eventObj.description ? '<p>' + eventObj.description + '</p>' : '') +
|
||||
(eventObj.tags !== null && eventObj.tags.length > 0 ? '<span class="badge bg-green">' + eventObj.tags.join('</span> <span class="badge bg-green">') + '</span>' : '') +
|
||||
'</div>'
|
||||
;
|
||||
}
|
||||
|
||||
function showPopover()
|
||||
{
|
||||
return window._hidePopovers === false;
|
||||
}
|
||||
|
||||
function activatePopover()
|
||||
{
|
||||
window._hidePopovers = false;
|
||||
}
|
||||
|
||||
function deactivatePopover()
|
||||
{
|
||||
window._hidePopovers = true;
|
||||
}
|
||||
|
||||
function hidePopover()
|
||||
{
|
||||
jQuery('.popover').popover('hide');
|
||||
}
|
||||
|
||||
let changeHandler = function(event, delta, revertFunc) {
|
||||
let updateUrl = '{{ path('patch_timesheet', {id: '-XX-'}) }}'.replace('-XX-', event.id);
|
||||
let API = kimai.getPlugin('api');
|
||||
let ALERT = kimai.getPlugin('alert');
|
||||
|
||||
let payload = {'begin': event.start.format()};
|
||||
|
||||
if (event.end !== null && event.end !== undefined) {
|
||||
payload.end = event.end.format();
|
||||
} else {
|
||||
payload.end = null;
|
||||
}
|
||||
|
||||
API.patch(updateUrl, JSON.stringify(payload), function(result) {
|
||||
ALERT.success('action.update.success');
|
||||
}, function(xhr, err) {
|
||||
const callback = API.getPatchErrorHandler();
|
||||
revertFunc();
|
||||
callback(xhr, err);
|
||||
});
|
||||
};
|
||||
const reloader = function() { calendar.reloadEvents(); };
|
||||
document.addEventListener('kimai.timesheetUpdate', reloader);
|
||||
document.addEventListener('kimai.timesheetDelete', reloader);
|
||||
|
||||
document.addEventListener('kimai.initialized', function(event) {
|
||||
// prevent multiple open popovers
|
||||
jQuery('html').on('click', function(e) {
|
||||
jQuery('.popover').each(function() {
|
||||
if(jQuery(e.target).parents(".fc-time-grid-event").get(0) !== $(this).prev().get(0)) {
|
||||
jQuery(this).popover('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
const kimai = event.detail.kimai;
|
||||
|
||||
{# convert HTML to draggable items #}
|
||||
jQuery('.external-events .external-event').each(function () {
|
||||
{# FullCalendar 3 can only work with jQuery UI elements #}
|
||||
jQuery(this).draggable({
|
||||
zIndex : 1070,
|
||||
revert : true, {# will cause the event to go back to its #}
|
||||
revertDuration: 0 {# original position after the drag #}
|
||||
});
|
||||
});
|
||||
|
||||
let kimai = event.detail.kimai;
|
||||
let API = kimai.getPlugin('api');
|
||||
let DATES = kimai.getPlugin('date');
|
||||
jQuery('{{ calendarSelector }}').fullCalendar({
|
||||
themeSystem: 'bootstrap3',
|
||||
defaultView: '{{ app.user.getPreferenceValue('calendar.initial_view') }}',
|
||||
let calendarOptions = {
|
||||
dragdrop: {
|
||||
container: '.external-events',
|
||||
items: '.external-event',
|
||||
},
|
||||
initialView: '{{ app.user.getPreferenceValue('calendar_initial_view') }}',
|
||||
translations: {
|
||||
customer: '{{ 'customer'|trans }}',
|
||||
project: '{{ 'project'|trans }}',
|
||||
activity: '{{ 'activity'|trans }}',
|
||||
},
|
||||
permissions: {
|
||||
edit: {% if canEdit %}true{% else %}false{% endif %},
|
||||
create: {% if canCreate %}true{% else %}false{% endif %},
|
||||
edit_begin: {% if can_edit_begin %}true{% else %}false{% endif %},
|
||||
edit_end: {% if can_edit_end %}true{% else %}false{% endif %},
|
||||
edit_duration: {% if can_edit_duration %}true{% else %}false{% endif %},
|
||||
punch: {% if is_punch_mode %}true{% else %}false{% endif %},
|
||||
},
|
||||
icons: {
|
||||
next: '{{ 'right'|icon(true) }}',
|
||||
previous: '{{ 'left'|icon(true) }}',
|
||||
nextYear: '{{ 'right2'|icon(true) }}',
|
||||
previousYear: '{{ 'left2'|icon(true) }}',
|
||||
},
|
||||
{% if google is not null %}
|
||||
googleCalendarApiKey: '{{ google.apiKey }}',
|
||||
{% endif %}
|
||||
header : {
|
||||
left : 'prev,next today{% if canDelete %} deleteButton{% endif %}',
|
||||
center: 'title',
|
||||
right : 'month,agendaWeek,agendaDay'
|
||||
patterns: {
|
||||
title: '{{ config.entryTitlePattern }}',
|
||||
},
|
||||
dragRevertDuration: 0,
|
||||
eventLimit: {{ config.dayLimit }},
|
||||
weekNumbersWithinDays: true,
|
||||
weekNumberCalculation: 'ISO',
|
||||
weekNumbers: {% if config.showWeekNumbers %}true{% else %}false{% endif %},
|
||||
allDaySlot: false,
|
||||
navLinks: true,
|
||||
locale: kimai.getConfiguration().get('locale').replace('_', '-').toLowerCase(),
|
||||
firstDay: kimai.getConfiguration().get('first_dow_iso'),
|
||||
customButtons: {
|
||||
deleteButton: {
|
||||
text: '{{ 'action.delete'|trans }}',
|
||||
click: function() {
|
||||
kimai.getPlugin('alert').info('{{ 'calendar.drag_and_drop.delete'|trans }}');
|
||||
}
|
||||
}
|
||||
},
|
||||
height: 'auto',
|
||||
nowIndicator: true,
|
||||
defaultStartTime: {% if defaultStartTime is null %}null{% else %}'{{ defaultStartTime }}'{% endif %},
|
||||
now: '{{ now|date_format('c') }}',
|
||||
{# see https://github.com/kimai/kimai/issues/2155 #}
|
||||
timezone: '{{ app.user.timezone == 'UTC' ? 'GMT' : app.user.timezone }}',
|
||||
weekends: {% if config.showWeekends %}true{% else %}false{% endif %},
|
||||
businessHours: {
|
||||
dow: [{{ config.businessDays|join(',') }}],
|
||||
start: '{{ config.businessTimeBegin }}',
|
||||
end: '{{ config.businessTimeEnd }}'
|
||||
},
|
||||
slotDuration: '{{ config.slotDuration }}',
|
||||
slotLabelInterval: '{{ config.slotDuration }}',
|
||||
minTime: '{{ config.timeframeBegin }}',
|
||||
maxTime: '{{ config.timeframeEnd }}',
|
||||
defaultTimedEventDuration: '{{ config.slotDuration }}', {# how long should entries look like when they don't have an end #}
|
||||
droppable: true,
|
||||
{# drop function handles external draggable events #}
|
||||
drop: function (date, jsEvent, ui, resourceId) {
|
||||
let entry = jsEvent.target;
|
||||
let source = entry.parentElement;
|
||||
let plainData = entry.dataset.entry;
|
||||
let data = JSON.parse(plainData);
|
||||
{% if isForeignEdit and canCreate %}
|
||||
data.user = {{ user.id }};
|
||||
{% endif %}
|
||||
let urlReplacer = JSON.parse(source.dataset.routeReplacer);
|
||||
let apiUrl = source.dataset.route;
|
||||
|
||||
for (const [key, value] of Object.entries(urlReplacer)) {
|
||||
apiUrl = apiUrl.replace(key, data[value]);
|
||||
}
|
||||
|
||||
let view = jQuery('{{ calendarSelector }}').fullCalendar('getView');
|
||||
let begin = date.clone();
|
||||
|
||||
{#
|
||||
FIXME some tracking modes do not allow to set the time
|
||||
|
||||
public function canEditEnd(): bool;
|
||||
public function canEditDuration(): bool;
|
||||
public function canUpdateTimesWithAPI(): bool;
|
||||
|
||||
#}
|
||||
|
||||
{% if not can_edit_begin %}
|
||||
begin.time('{{ defaultStartTime }}');
|
||||
{% endif %}
|
||||
|
||||
if (view.name === 'month' && !begin.hasTime()) {
|
||||
begin.time('{{ defaultStartTime }}');
|
||||
}
|
||||
|
||||
let end = begin.clone();
|
||||
end.add(moment.duration('{{ config.slotDuration }}'));
|
||||
|
||||
{% if not is_punch_mode %}
|
||||
{% if can_edit_begin %}
|
||||
data.begin = begin.format();
|
||||
{% endif %}
|
||||
{% if can_edit_end %}
|
||||
data.end = end.format();
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
if (source.dataset.method === 'PATCH') {
|
||||
API.patch(
|
||||
apiUrl,
|
||||
JSON.stringify(data),
|
||||
function (result) {
|
||||
kimai.getPlugin('alert').success('action.update.success');
|
||||
let newItem = convertApiTimesheetToCalendar(result);
|
||||
jQuery('{{ calendarSelector }}').fullCalendar('renderEvent', newItem);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
API.post(
|
||||
apiUrl,
|
||||
JSON.stringify(data),
|
||||
function (result) {
|
||||
kimai.getPlugin('alert').success('action.update.success');
|
||||
let newItem = convertApiTimesheetToCalendar(result);
|
||||
jQuery('{{ calendarSelector }}').fullCalendar('renderEvent', newItem);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
eventSources: [
|
||||
{
|
||||
events: function(start, end, timezone, callback) {
|
||||
let from = moment(start.format()).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
|
||||
let to = moment(end.format()).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
|
||||
API.get('{{ path('get_timesheets') }}?user={{ user.id }}&size=1000&full=true&begin='+from+'&end='+to, {}, function(result) {
|
||||
let apiEvents = [];
|
||||
for (const record of result) {
|
||||
apiEvents.push(convertApiTimesheetToCalendar(record));
|
||||
}
|
||||
callback(apiEvents);
|
||||
});
|
||||
id: 'kimaiUserTimeSource',
|
||||
type: 'timesheet',
|
||||
url: '{{ path('get_timesheets') }}?user={{ user.id }}&size=1000&full=true&begin={from}&end={to}',
|
||||
options: {
|
||||
color: '{{ config('theme.calendar.background_color') }}',
|
||||
},
|
||||
color: '{{ config('theme.calendar.background_color') }}',
|
||||
name: 'kimaiUserTimeSource'
|
||||
}
|
||||
{% if google is not null %}
|
||||
{% for source in google.sources %}
|
||||
,
|
||||
{
|
||||
{% for source in google.sources %}
|
||||
,
|
||||
{
|
||||
id: 'googleCompanyCalendar',
|
||||
type: 'google',
|
||||
options: {
|
||||
googleCalendarId: '{{ source.uri }}',
|
||||
name: '{{ source.id }}',
|
||||
color: '{{ source.color }}',
|
||||
textColor: '{{ source.color|font_contrast }}',
|
||||
editable: false
|
||||
}
|
||||
{% endfor %}
|
||||
}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
],
|
||||
eventRender: function(eventObj, $el, view) {
|
||||
{# don't show popover for google calendar #}
|
||||
if (eventObj.source.ajaxSettings !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
{# or when an event is dragged or resized #}
|
||||
if (!showPopover()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.name !== 'month' ) {
|
||||
jQuery(".fc-dailytotal").text(DATES.formatSeconds(0)).data('seconds', 0).data('ids', '');
|
||||
}
|
||||
|
||||
hidePopover();
|
||||
$el.popover({
|
||||
title: renderEventPopoverTitle(eventObj),
|
||||
content: renderEventPopoverContent(eventObj),
|
||||
trigger: 'hover',
|
||||
placement: 'auto',
|
||||
container: 'body',
|
||||
html: true
|
||||
});
|
||||
},
|
||||
viewRender: function(view, element) {
|
||||
if (view.name === 'month') {
|
||||
return;
|
||||
}
|
||||
|
||||
{# allowed in agendaWeek and agendaDay #}
|
||||
jQuery(".fc-day-header").each(function (key, val) {
|
||||
var $this = jQuery(this);
|
||||
var dateYMD = $this.attr("data-date");
|
||||
$this.append('<div class="fc-dailytotal" data-seconds="0" data-ids="" id="dailytotal-' + dateYMD + '">'+DATES.formatSeconds(0)+'</div>');
|
||||
});
|
||||
},
|
||||
eventAfterRender: function(event, element, view) {
|
||||
{# allowed in agendaWeek and agendaDay #}
|
||||
if (view.name === 'month') {
|
||||
return;
|
||||
}
|
||||
|
||||
{# TODO calculate duration for running entries #}
|
||||
if (event.end === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
{# TODO split day entries are calculated only for the begin date #}
|
||||
var $element = jQuery("#dailytotal-" + moment(event.start).format("YYYY-MM-DD"));
|
||||
var ids = $element.data('ids');
|
||||
if (ids === null || ids === undefined) {
|
||||
ids = '';
|
||||
}
|
||||
|
||||
if (ids.indexOf(event.id) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var prev = $element.data('seconds');
|
||||
var duration = prev + ((event.end - event.start) / 1000);
|
||||
$element.data('seconds', duration);
|
||||
$element.data('ids', event.id + ' ' + ids);
|
||||
$element.text(DATES.formatSeconds(duration));
|
||||
},
|
||||
{% if not is_punch_mode and canCreate %}
|
||||
dayClick: function(date, jsEvent, view) {
|
||||
{#
|
||||
Day-clicks are always triggered, unless a selection was created.
|
||||
So clicking in a day (month view) or any slot (week and day view) will trigger a dayClick
|
||||
BEFORE triggering a select - make sure not two create dialogs are requested
|
||||
#}
|
||||
if (view.type !== 'month') {
|
||||
return;
|
||||
}
|
||||
|
||||
let createUrl = '{{ path(createRoute) }}?begin=' + date.format() + '{{ createParams|raw }}';
|
||||
kimai.getPlugin('modal').openUrlInModal(createUrl);
|
||||
url: {
|
||||
update: (timesheetId) => {
|
||||
return '{{ path('patch_timesheet', {id: 1}) }}'.replace('1', timesheetId);
|
||||
},
|
||||
selectable: true,
|
||||
select: function(start, end, jsEvent, view) {
|
||||
if(view.type === 'month') {
|
||||
{#
|
||||
Multi-day clicks are NOT allowed in the month view, as simple day clicks would also trigger
|
||||
a select - there is no way to distinguish a simple click and a two day selection
|
||||
#}
|
||||
return;
|
||||
create: (start, end) => {
|
||||
let createUrl = '{{ path(createRoute) }}' + '?{{ createParams|raw }}';
|
||||
if (end !== undefined) {
|
||||
createUrl += 'from=' + start + '&to=' + end;
|
||||
} else {
|
||||
createUrl += 'begin=' + start;
|
||||
}
|
||||
let createUrl = '{{ path(createRoute) }}' + '?from=' + start.format() + '&to=' + end.format() + '{{ createParams|raw }}';
|
||||
kimai.getPlugin('modal').openUrlInModal(createUrl);
|
||||
return createUrl;
|
||||
},
|
||||
{% endif %}
|
||||
{% if canEdit %}
|
||||
eventClick: function(eventObj, jsEvent, view) {
|
||||
if (eventObj.source.ajaxSettings !== undefined) {
|
||||
jsEvent.preventDefault();
|
||||
return;
|
||||
}
|
||||
let editUrl = '{{ path(editRoute, {id: '-XX-'}) }}'.replace('-XX-', eventObj.id);
|
||||
kimai.getPlugin('modal').openUrlInModal(editUrl);
|
||||
edit: (timesheetId) => {
|
||||
return '{{ path(editRoute, {id: '-XX-'}) }}'.replace('-XX-', timesheetId);
|
||||
},
|
||||
{% if not is_punch_mode %}
|
||||
editable: true,
|
||||
eventDragStart: function(event, jsEvent, ui, view) {
|
||||
deactivatePopover();
|
||||
},
|
||||
eventDragStop: function(event, jsEvent, ui, view) {
|
||||
activatePopover();
|
||||
{% if canDelete %}
|
||||
var trash = jQuery('.fc-deleteButton-button');
|
||||
var offset = trash.offset();
|
||||
|
||||
if (jsEvent.pageX >= offset.left && jsEvent.pageX<= (offset.left + trash.outerWidth(true)) &&
|
||||
jsEvent.pageY >= offset.top && jsEvent.pageY <= (offset.top + trash.outerHeight(true))) {
|
||||
API.delete(
|
||||
'{{ path('delete_timesheet', {'id': '__xxx__'}) }}'.replace('__xxx__', event.id),
|
||||
function () {
|
||||
kimai.getPlugin('alert').success('action.delete.success');
|
||||
jQuery('{{ calendarSelector }}').fullCalendar('removeEvents', event._id);
|
||||
}, function () {
|
||||
kimai.getPlugin('alert').success('action.delete.error');
|
||||
}
|
||||
);
|
||||
}
|
||||
{% endif %}
|
||||
},
|
||||
eventDrop: changeHandler,
|
||||
eventResizeStart: function(event, jsEvent, ui, view) {
|
||||
deactivatePopover();
|
||||
},
|
||||
eventResizeStop: function(event, jsEvent, ui, view) {
|
||||
activatePopover();
|
||||
},
|
||||
eventResize: changeHandler,
|
||||
actions: (timesheetId) => {
|
||||
return '{{ path('get_timesheet_actions', {id: 1, 'view': 'calendar', 'locale': (app.request.locale)}) }}'.replace('1', timesheetId);
|
||||
},
|
||||
},
|
||||
preparePayloadForUpdate: (data) => {
|
||||
{% if isForeignEdit %}
|
||||
data.user = {{ user.id }};
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
});
|
||||
var trashBtn = jQuery('.fc-deleteButton-button');
|
||||
trashBtn.html('<span class="text-danger"><i class="{{ 'trash'|icon }} text-danger"></i> ' + trashBtn.text() + '</span>');
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
calendar = new KimaiCalendar(
|
||||
kimai,
|
||||
document.getElementById('{{ calendarSelector }}'),
|
||||
Object.assign({}, calendarOptions, {{ config|json_encode|raw }})
|
||||
);
|
||||
calendar.render();
|
||||
|
||||
document.querySelector('.fc-today-button').classList.remove('btn-icon');
|
||||
document.querySelector('.fc-header-toolbar').classList.toggle('row');
|
||||
document.querySelector('.fc-header-toolbar').classList.add('p-3');
|
||||
const toolbar = document.querySelectorAll('.fc-header-toolbar .fc-toolbar-chunk');
|
||||
toolbar[0].parentElement.classList.add('p-3');
|
||||
toolbar[0].classList.add('col-md-4', 'col-6', 'mb-1', 'text-start', 'order-xs-1');
|
||||
toolbar[1].classList.add('col-md-4', 'col-12', 'mb-1', 'text-center', 'order-3');
|
||||
toolbar[2].classList.add('col-md-4', 'col-6', 'mb-1', 'text-end', 'order-md-4', 'order-xs-2');
|
||||
|
||||
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user