leantime.ticketsController = (function () { //Variables //Functions function countTickets() { let ticketCounts = []; jQuery(".sortableTicketList").each(function (indexList) { jQuery(this).find(".column").each(function (indexCol) { if (ticketCounts[indexCol] === undefined) { ticketCounts[indexCol] = 0; } var counting = jQuery(this).find('.moveable').length; ticketCounts[indexCol] += counting; }); }); jQuery(".widgettitle .count").each(function (index) { jQuery(this).text(ticketCounts[index]); }); } /** * Update swimlane row counts after card movement * Counts tickets in each swimlane row and updates the count badges */ function updateSwimlaneCounts() { jQuery(".kanban-swimlane-row").each(function() { var $row = jQuery(this); var swimlaneId = $row.attr('id'); if (!swimlaneId) return; // Extract the swimlane identifier from the row ID (e.g., "swimlane-row-3" -> "3") var swimlaneKey = swimlaneId.replace('swimlane-row-', ''); // Find the swimlane content area and count tickets var $content = $row.find('.kanban-swimlane-content'); var ticketCount = $content.find('.moveable').length; // Update all count badges in this swimlane's sidebar var $sidebar = $row.find('.kanban-swimlane-sidebar'); $sidebar.find('.kanban-lane-count').text(ticketCount); // Update the aria-label for accessibility var currentLabel = $sidebar.attr('aria-label') || ''; var newLabel = currentLabel.replace(/\d+ tasks/, ticketCount + ' tasks'); $sidebar.attr('aria-label', newLabel); }); // Also update the progress bars updateSwimlaneProgressBars(); } /** * Update swimlane progress bars after card movement * Recalculates status distribution and updates the micro-progress-bar segments */ function updateSwimlaneProgressBars() { jQuery(".kanban-swimlane-row").each(function() { var $row = jQuery(this); var $content = $row.find('.kanban-swimlane-content'); var $progressBar = $row.find('.micro-progress-bar .progress-segments'); if (!$progressBar.length) return; // Count tickets per status in this swimlane var statusCounts = {}; var totalCount = 0; $content.find('.contentInner').each(function() { var $column = jQuery(this); var classAttr = $column.attr('class') || ''; var statusMatch = classAttr.match(/status_(-?\d+)/); if (statusMatch) { var statusId = statusMatch[1]; var ticketCount = $column.find('.moveable').length; if (ticketCount > 0) { statusCounts[statusId] = ticketCount; totalCount += ticketCount; } } }); // Get existing segments and their status IDs var $segments = $progressBar.find('.status-segment'); if (totalCount === 0) { // No tickets - hide all segments $segments.css('flex', '0 1 0%').find('.segment-count').text(''); $segments.attr('data-tippy-content', ''); return; } // Update each segment's flex-grow proportionally $segments.each(function() { var $segment = jQuery(this); var classAttr = $segment.attr('class'); if (!classAttr) return; var statusMatch = classAttr.match(/status-(-?\d+)/); if (statusMatch) { var statusId = statusMatch[1]; var count = statusCounts[statusId] || 0; var percentage = (count / totalCount) * 100; // Use flex-grow proportionally so segments fill 100% without gaps $segment.css('flex', percentage + ' 1 0%'); // Update count text var $countSpan = $segment.find('.segment-count'); if ($countSpan.length) { $countSpan.text(count > 0 ? count : ''); } // Update tooltip - only show if count > 0 var currentTooltip = $segment.attr('data-tippy-content') || ''; var labelMatch = currentTooltip.match(/^([^:]+):/); var label = labelMatch ? labelMatch[1] : 'Status ' + statusId; $segment.attr('data-tippy-content', count > 0 ? label + ': ' + count : ''); } }); }); } /** * Move a ticket card to a different swimlane when grouped field is changed * @param {number} ticketId - The ticket ID * @param {string|number} newSwimlaneValue - The new swimlane value (priority, milestone, etc.) */ var moveCardToSwimlane = function(ticketId, newSwimlaneValue) { // Find the card element var $card = jQuery("#ticket_" + ticketId); if (!$card.length) { console.warn("Card not found for ticket ID:", ticketId); return; } // Get current status from card's column var $currentColumn = $card.closest('.contentInner'); var classAttr = $currentColumn.attr('class') || ''; var statusMatch = classAttr.match(/status_(\d+)/); if (!statusMatch) { console.warn("Could not determine status for ticket:", ticketId); location.reload(); // Fallback to reload return; } var statusId = statusMatch[1]; // Find target swimlane column var $targetColumn = jQuery("#kanboard-" + newSwimlaneValue + " .contentInner.status_" + statusId); if (!$targetColumn.length) { console.warn("Target swimlane not found:", newSwimlaneValue, statusId); location.reload(); // Fallback to reload return; } // Don't move if already in correct location if ($currentColumn[0] === $targetColumn[0]) { return; } // Add exit animation $card.addClass('card-moving-out'); setTimeout(function() { // Move card to new swimlane $card.detach().appendTo($targetColumn); // Add entrance animation $card.removeClass('card-moving-out').addClass('card-moving-in'); // Update ticket counts countTickets(); updateSwimlaneCounts(); // Refresh sortable to recalculate positions if ($targetColumn.sortable) { $targetColumn.sortable('refresh'); } // Remove animation class after transition setTimeout(function() { $card.removeClass('card-moving-in'); }, 300); }, 200); }; var updateRemainingHours = function (element, id) { var value = jQuery(element).val(); leantime.ticketsRepository.updateRemainingHours( id, value, function () { jQuery.growl({message: leantime.i18n.__("short_notifications.remaining_hours_updated"), style: "success"}); } ); }; var updatePlannedHours = function (element, id) { var value = jQuery(element).val(); leantime.ticketsRepository.updatePlannedHours( id, value, function () { jQuery.growl({message: leantime.i18n.__("short_notifications.planned_hours_updated"), style: "success"}); } ); }; var toggleFilterBar = function () { jQuery(".filterBar").toggle(); }; var initGanttChart = function (tasks, viewMode, readonly) { function htmlEntities(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } jQuery(document).ready( function () { // The Gantt constructor fires on_view_change once while building. Persisting that // event would overwrite the user's saved scale with the default, so ignore any // view change until construction is finished; only user-driven changes save. var ganttInitializing = true; if (readonly === false) { var gantt_chart = new Gantt( "#gantt", tasks, { header_height: 55, column_width: 20, step: 24, view_modes: ['Day', 'Week', 'Month'], bar_height: 40, static_progress_indicator: true, bar_corner_radius: 10, arrow_curve: 10, padding:20, view_mode: viewMode, date_format: leantime.i18n.__("language.momentJSDate"), language: leantime.i18n.__("language.code").slice(0, 2), //Get first 2 characters of language code additional_rows: 5, custom_popup_html: function (task) { // the task object will contain the updated // dates and progress value var end_date = task._end; var dateObject = new Date(end_date); var dateTime = jQuery.datepicker.formatDate(leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), new Date(end_date)); var popUpHTML = '
'; if (task.projectName !== undefined) { popUpHTML += '

' + task.projectName + '

'; } popUpHTML += '' + task.type + ' #' + task.id + ' '; if (task.type === 'milestone') { popUpHTML += '

' + htmlEntities(task.name) + '


' + '

' + leantime.i18n.__("text.expected_to_finish_by") + ' ' + dateTime + '
' + '' + Math.round(task.progress) + '%

' + ' ' + leantime.i18n.__("links.edit_milestone") + ' | ' + ' ' + leantime.i18n.__("links.view_todos") + ' '; } else { popUpHTML += '

' + htmlEntities(task.name) + '


' + ' ' + leantime.i18n.__("links.edit_todo") + ' '; } popUpHTML += '
'; return popUpHTML; }, on_click: function (task) { }, on_date_change: function (task, start, end) { leantime.ticketsRepository.updateMilestoneDates(task.id, start, end, task._index+1); }, on_sort_change: function (tasks) { var sortPayload = {}; for (var i = 0; i < tasks.length; i++) { //start sorting counter at 1 instead of 0 since 0 will cause date comparison sortPayload[tasks[i].id] = tasks[i]._index+1; } leantime.rpc('Tickets.Tickets.sortTickets', { params: sortPayload }) .catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not sort tickets', error); }); }, on_progress_change: function (task, progress) { //_initModals(); }, on_view_change: function (mode) { if (ganttInitializing) { return; } leantime.usersRepository.updateUserViewSettings("roadmap", mode); }, on_popup_show: function (task) { } } ); } else { var gantt_chart = new Gantt( "#gantt", tasks, { readonlyGantt: true, resizing: false, progress: false, is_draggable: false, view_modes: ['Day', 'Week', 'Month'], view_mode: viewMode, custom_popup_html: function (task) { var end_date = task._end; var dateObject = new Date(end_date); var dateTime = jQuery.datepicker.formatDate(leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), new Date(end_date)); var popUpHTML = '
'; if (task.projectName !== undefined) { popUpHTML += '

' + task.projectName + '

'; } popUpHTML += '' + task.type + ' #' + task.id + ' '; if (task.type === 'milestone') { popUpHTML += '

' + htmlEntities(task.name) + '


' + '

' + leantime.i18n.__("text.expected_to_finish_by") + ' ' + dateTime + '
' + '' + Math.round(task.progress) + '%

' + ' ' + leantime.i18n.__("links.view_todos") + ' '; } else { popUpHTML += '

' + htmlEntities(task.name) + '


' + ' ' + leantime.i18n.__("links.edit_todo") + ' '; } popUpHTML += '
'; return popUpHTML; }, on_click: function (task) { }, on_date_change: function (task, start, end) { }, on_progress_change: function (task, progress) { }, on_view_change: function (mode) { if (ganttInitializing) { return; } leantime.usersRepository.updateUserViewSettings("roadmap", mode); } } ); } jQuery("#ganttTimeControl").on( "click", "a", function () { var $btn = jQuery(this); var mode = $btn.attr("data-value"); gantt_chart.change_view_mode(mode); $btn.parent().parent().find('a').removeClass('active'); $btn.addClass('active'); var label = $btn.text(); jQuery(".viewText").text(label); } ); // Constructor already built the chart at the saved viewMode; construction is done, // so from here on real user-driven view changes are allowed to persist. ganttInitializing = false; } ); }; var initSprintDates = function () { Date.prototype.addDays = function (days) { this.setDate(this.getDate() + days); return this; }; jQuery.datepicker.setDefaults( { beforeShow: function (i) { if (jQuery(i).attr('readonly')) { return false; } } } ); var dateFormat = leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), from = jQuery("#sprintStart") .datepicker( { numberOfMonths: 1, dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dayNames: leantime.i18n.__("language.dayNames").split(","), dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","), dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","), monthNames: leantime.i18n.__("language.monthNames").split(","), currentText: leantime.i18n.__("language.currentText"), closeText: leantime.i18n.__("language.closeText"), buttonText: leantime.i18n.__("language.buttonText"), isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0, nextText: leantime.i18n.__("language.nextText"), prevText: leantime.i18n.__("language.prevText"), weekHeader: leantime.i18n.__("language.weekHeader"), firstDay: leantime.i18n.__("language.firstDayOfWeek"), } ) .on( "change", function () { to.datepicker("option", "minDate", getDate(this)); var newEndDate = getDate(this).addDays(13); to.datepicker('setDate', newEndDate); //set date } ), to = jQuery("#sprintEnd").datepicker( { defaultDate: "+1w", numberOfMonths: 1, dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dayNames: leantime.i18n.__("language.dayNames").split(","), dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","), dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","), monthNames: leantime.i18n.__("language.monthNames").split(","), currentText: leantime.i18n.__("language.currentText"), closeText: leantime.i18n.__("language.closeText"), buttonText: leantime.i18n.__("language.buttonText"), isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0, nextText: leantime.i18n.__("language.nextText"), prevText: leantime.i18n.__("language.prevText"), weekHeader: leantime.i18n.__("language.weekHeader"), firstDay: leantime.i18n.__("language.firstDayOfWeek"), } ) .on( "change", function () { from.datepicker("option", "maxDate", getDate(this)); } ); function getDate( element ) { var date; try { date = jQuery.datepicker.parseDate(dateFormat, element.value); } catch ( error ) { date = null; console.log(error); } return date; } }; var _initMilestoneDates = function () { var dateFormat = leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), from = jQuery("#milestoneEditFrom") .datepicker( { numberOfMonths: 1, dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dayNames: leantime.i18n.__("language.dayNames").split(","), dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","), dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","), monthNames: leantime.i18n.__("language.monthNames").split(","), currentText: leantime.i18n.__("language.currentText"), closeText: leantime.i18n.__("language.closeText"), buttonText: leantime.i18n.__("language.buttonText"), isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0, nextText: leantime.i18n.__("language.nextText"), prevText: leantime.i18n.__("language.prevText"), weekHeader: leantime.i18n.__("language.weekHeader"), firstDay: leantime.i18n.__("language.firstDayOfWeek"), } ) .on( "change", function () { to.datepicker("option", "minDate", getDate(this)); } ), to = jQuery("#milestoneEditTo").datepicker( { defaultDate: "+1w", numberOfMonths: 1, dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dayNames: leantime.i18n.__("language.dayNames").split(","), dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","), dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","), monthNames: leantime.i18n.__("language.monthNames").split(","), currentText: leantime.i18n.__("language.currentText"), closeText: leantime.i18n.__("language.closeText"), buttonText: leantime.i18n.__("language.buttonText"), isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0, nextText: leantime.i18n.__("language.nextText"), prevText: leantime.i18n.__("language.prevText"), weekHeader: leantime.i18n.__("language.weekHeader"), firstDay: leantime.i18n.__("language.firstDayOfWeek"), } ) .on( "change", function () { from.datepicker("option", "maxDate", getDate(this)); } ); function getDate( element ) { var date; try { date = jQuery.datepicker.parseDate(dateFormat, element.value); } catch ( error ) { date = null; console.log(error); } return date; } }; var initMilestoneDatesAsyncUpdate = function () { var dateFormat = leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), from = jQuery(".milestoneEditFromAsync") .datepicker( { numberOfMonths: 1, dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dayNames: leantime.i18n.__("language.dayNames").split(","), dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","), dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","), monthNames: leantime.i18n.__("language.monthNames").split(","), currentText: leantime.i18n.__("language.currentText"), closeText: leantime.i18n.__("language.closeText"), buttonText: leantime.i18n.__("language.buttonText"), isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0, nextText: leantime.i18n.__("language.nextText"), prevText: leantime.i18n.__("language.prevText"), weekHeader: leantime.i18n.__("language.weekHeader"), firstDay: leantime.i18n.__("language.firstDayOfWeek"), } ) .on( "change", function () { var date = jQuery(this).val(); var id = jQuery(this).attr("data-id"); var toDatePicker = jQuery(".toDateTicket-" + id); toDatePicker.datepicker("option", "minDate", getDate(this)); var dateTo = jQuery(".toDateTicket-" + id).val(); leantime.ticketsRepository.updateEditFromDates(id, date, function() { jQuery.growl({message: leantime.i18n.__("short_notifications.date_updated"), style: "success"}); }); } ), to = jQuery(".milestoneEditToAsync").datepicker( { defaultDate: "+1w", numberOfMonths: 1, dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dayNames: leantime.i18n.__("language.dayNames").split(","), dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","), dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","), monthNames: leantime.i18n.__("language.monthNames").split(","), currentText: leantime.i18n.__("language.currentText"), closeText: leantime.i18n.__("language.closeText"), buttonText: leantime.i18n.__("language.buttonText"), isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0, nextText: leantime.i18n.__("language.nextText"), prevText: leantime.i18n.__("language.prevText"), weekHeader: leantime.i18n.__("language.weekHeader"), firstDay: leantime.i18n.__("language.firstDayOfWeek"), } ) .on( "change", function () { var id = jQuery(this).attr("data-id"); var fromDateTicket = jQuery(".fromDateTicket-" + id); fromDateTicket.datepicker("option", "maxDate", getDate(this)); var date = jQuery(this).val(); var dateFrom = jQuery(".fromDateTicket-" + id).val(); leantime.ticketsRepository.updateEditToDates(id, date, function() { jQuery.growl({message: leantime.i18n.__("short_notifications.date_updated"), style: "success"}); }); } ); function getDate( element ) { var date; try { date = jQuery.datepicker.parseDate(dateFormat, element.value); } catch ( error ) { date = null; console.log(error); } return date; } }; var initToolTips = function () { jQuery('[data-toggle="tooltip"]').tooltip(); }; var initEffortDropdown = function () { var storyPointLabels = { '0.5': '< 2min', '1': 'XS', '2': 'S', '3': "M", '5': "L", '8' : "XL", '13': "XXL" }; jQuery(".effortDropdown .dropdown-menu a").unbind().on("click", function () { var dataValue = jQuery(this).attr("data-value").split("_"); if (dataValue.length === 2) { var ticketId = dataValue[0]; var effortId = dataValue[1]; leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: { storypoints: effortId } }) .then( function () { jQuery("#effortDropdownMenuLink" + ticketId + " span.text").text(storyPointLabels[effortId]); jQuery.growl({message: leantime.i18n.__("short_notifications.effort_updated"), style: "success"}); // Move card to correct swimlane if grouped by effort if (leantime.kanbanGroupBy === 'storypoints') { moveCardToSwimlane(ticketId, effortId); } } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + ticketId, error); }); } else { console.log("Ticket Controller: Effort data value not set correctly"); } }); }; var initPriorityDropdown = function () { // '1' => 'Critical', '2' => 'High', '3' => 'Medium', '4' => 'Low' var priorityLabels = { '1': 'Critical', '2': 'High', '3': "Medium", '4': "Low", '5': "Lowest" }; jQuery(".priorityDropdown .dropdown-menu a").unbind().on("click", function () { var dataValue = jQuery(this).attr("data-value").split("_"); if (dataValue.length === 2) { var ticketId = dataValue[0]; var priorityId = dataValue[1]; leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: { priority: priorityId } }) .then( function () { jQuery("#priorityDropdownMenuLink" + ticketId + " span.text").text(priorityLabels[priorityId]); jQuery("#priorityDropdownMenuLink" + ticketId + "").removeClass("priority-bg-1 priority-bg-2 priority-bg-3 priority-bg-4 priority-bg-5"); jQuery("#priorityDropdownMenuLink" + ticketId + "").addClass("priority-bg-" + priorityId); jQuery("#priorityDropdownMenuLink" + ticketId + "").parents(".ticketBox").removeClass("priority-border-1 priority-border-2 priority-border-3 priority-border-4 priority-border-5"); jQuery("#priorityDropdownMenuLink" + ticketId + "").parents(".ticketBox").addClass("priority-border-" + priorityId); jQuery.growl({message: leantime.i18n.__("short_notifications.priority_updated"), style: "success"}); // Move card to correct swimlane if grouped by priority if (leantime.kanbanGroupBy === 'priority') { moveCardToSwimlane(ticketId, priorityId); } } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + ticketId, error); }); } else { console.log("Ticket Controller: Priority data value not set correctly"); } }); }; var initMilestoneDropdown = function () { jQuery(".milestoneDropdown .dropdown-menu a").unbind().on("click", function () { var dataValue = jQuery(this).attr("data-value").split("_"); var dataLabel = jQuery(this).attr('data-label'); if (dataValue.length === 3) { var ticketId = dataValue[0]; var milestoneId = dataValue[1]; var color = dataValue[2]; jQuery("#milestoneDropdownMenuLink" + ticketId + " span.text").append(" ..."); leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: { milestoneid: milestoneId } }) .then( function () { jQuery("#milestoneDropdownMenuLink" + ticketId + " span.text").text(dataLabel); jQuery("#milestoneDropdownMenuLink" + ticketId).css("backgroundColor", color); jQuery.growl({message: leantime.i18n.__("short_notifications.milestone_updated"), style: "success"}); // Move card to correct swimlane if grouped by milestone if (leantime.kanbanGroupBy === 'milestoneid') { moveCardToSwimlane(ticketId, milestoneId); } } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + ticketId, error); }); } }); }; var initStatusDropdown = function () { jQuery(".statusDropdown .dropdown-menu a").unbind().on("click", function () { var dataValue = jQuery(this).attr("data-value").split("_"); var dataLabel = jQuery(this).attr('data-label'); if (dataValue.length == 3) { var ticketId = dataValue[0]; var statusId = dataValue[1]; var className = dataValue[2]; leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: { status: statusId } }) .then( function () { jQuery("#statusDropdownMenuLink" + ticketId + " span.text").text(dataLabel); jQuery("#statusDropdownMenuLink" + ticketId).removeClass().addClass(className + " dropdown-toggle f-left status "); jQuery.growl({message: leantime.i18n.__("short_notifications.status_updated"), style: "success"}); } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + ticketId, error); }); } }); }; var initUserDropdown = function () { jQuery(".userDropdown .dropdown-menu a").unbind().on("click", function () { var dataValue = jQuery(this).attr("data-value").split("_"); var dataLabel = jQuery(this).attr('data-label'); if (dataValue.length === 3) { var ticketId = dataValue[0]; var userId = dataValue[1]; var profileImageId = dataValue[2]; leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: { editorId: userId } }) .then( function () { jQuery("#userDropdownMenuLink" + ticketId + " span.text span#userImage" + ticketId + " img").attr("src", leantime.appUrl + "/users/profileImage/" + encodeURIComponent(userId)); jQuery("#userDropdownMenuLink" + ticketId + " span.text span#user" + ticketId).text(dataLabel); jQuery.growl({message: leantime.i18n.__("short_notifications.user_updated"), style: "success"}); // Move card to correct swimlane if grouped by user if (leantime.kanbanGroupBy === 'editorId') { moveCardToSwimlane(ticketId, userId); } } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + ticketId, error); }); } }); }; var initAsyncInputChange = function () { jQuery(".asyncInputUpdate").on("change", function () { var dataLabel = jQuery(this).attr('data-label').split("-"); if (dataLabel.length == 2) { var fieldName = dataLabel[0]; var entityId = dataLabel[1]; var value = jQuery(this).val(); leantime.rpc('Tickets.Tickets.patchTicket', { id: entityId, values: { [fieldName]: value } }) .then( function () { jQuery.growl({message: leantime.i18n.__("notifications.subtask_saved"), style: "success"}); } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + entityId, error); }); } }); }; var initSprintDropdown = function () { jQuery(".sprintDropdown .dropdown-menu a").unbind().on("click", function () { var dataValue = jQuery(this).attr("data-value").split("_"); var dataLabel = jQuery(this).attr('data-label'); if (dataValue.length == 2) { var ticketId = dataValue[0]; var sprintId = dataValue[1]; leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: { sprint: sprintId } }) .then( function () { jQuery("#sprintDropdownMenuLink" + ticketId + " span.text").text(dataLabel); jQuery.growl({message: leantime.i18n.__("short_notifications.sprint_updated"), style: "success"}); // Move card to correct swimlane if grouped by sprint if (leantime.kanbanGroupBy === 'sprint') { moveCardToSwimlane(ticketId, sprintId); } } ).catch(function (error) { jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" }); console.error('Could not update ticket ' + ticketId, error); }); } }); }; var initSimpleColorPicker = function () { var colors = ['#821219', '#BB1B25', '#75BB1B', '#4B7811', '#fdab3d', '#1bbbb1', '#006d9f', '#124F7D', '#082236', '#5F0F40', '#bb1b75', '#F26CA7', '#BB611B', '#aaaaaa', '#4c4c4c', ]; jQuery('input.simpleColorPicker').simpleColorPicker( { colors: colors, onChangeColor: function (color) { jQuery(this).css('background', color); jQuery(this).css('color', "#fff"); } } ); var currentColor = jQuery('input.simpleColorPicker').val(); if (currentColor != '') { jQuery('input.simpleColorPicker').css('background', currentColor); } }; var initDueDateTimePickers = function () { // Reset due date by clicking a button on the task in the dashboard jQuery(".date-picker-form-control .reset-button").on('click', function () { // Ticket id for api patch call const id = jQuery(this).attr("data-id"); // Update date input to have "text-anytime" instead of old date jQuery("#due-date-picker-"+id).val(leantime.i18n.__("text.anytime")); // Set date to null to reset leantime.ticketsRepository.updateDueDates(id, null, function () { // Notify user that due date is updated jQuery.growl({message: leantime.i18n.__("short_notifications.duedate_updated"), style: "success"}); }); }); leantime.dateController.initDatePicker(".quickDueDates, .duedates", function(date, instance) { //TODO: Update to use htmx, this is awful var day = instance.currentDay; var month = instance.currentMonth; var year = instance.currentYear; var dateObject = new Date(year, month, day); var parsed = jQuery.datepicker.formatDate(leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"), dateObject); var id = jQuery(this).attr("data-id"); leantime.ticketsRepository.updateDueDates(id, parsed, function () { jQuery.growl({message: leantime.i18n.__("short_notifications.duedate_updated"), style: "success"}); }); }); }; var initTimeSheetChart = function (labels, d2, d3, canvasId) { var ctx = document.getElementById(canvasId).getContext('2d'); var stackedLine = new Chart(ctx, { type: 'line', data: { labels: labels, datasets:[{ label: leantime.i18n.__("label.booked_hours"), backgroundColor: 'rgba(201,48,44, 0.5)', borderColor: 'rgb(201,48,44)', data:d2 }, { label:leantime.i18n.__("label.planned_hours"), backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor:'rgb(54, 162, 235)', data:d3 }] }, options: { scales: { x: { display: true, title: { display: true, text: leantime.i18n.__("label.booked_hours"), }, type: 'time', time: { unit: 'day' } }, y: { display: true, title: { display: true, text: leantime.i18n.__("label.planned_hours") }, ticks: { beginAtZero: true } } } } }); }; var colorTicketBoxes = function (currentBox) { var color = "#fff"; jQuery(".ticketBox").each(function (index) { var value = jQuery(this).find(".statusDropdown > a").attr("class"); if (value != undefined) { if (value.indexOf("important") > -1) { color = "#b94a48"; } else if (value.indexOf("info") > -1) { color = "#2d6987"; } else if (value.indexOf("warning") > -1) { color = "#f89406"; } else if (value.indexOf("success") > -1) { color = "#468847"; } else if (value.indexOf("default") > -1) { color = "#999999"; } else { color = "#999999"; } jQuery(this).css("borderLeft", "5px solid " + color); if (currentBox != null) { if (jQuery(this).attr("data-val") == currentBox) { jQuery("#ticket_" + currentBox + " .ticketBox").animate({backgroundColor: color}, 'fast').animate({backgroundColor: "#fff"}, 'slow'); } } } }); }; var initTicketTabs = function () { jQuery(document).ready(function () { let url = new URL(window.location.href); let tab = url.searchParams.get("tab"); let activeTabIndex = 0; if (tab) { activeTabIndex = jQuery('.ticketTabs').find('a[href="#' + tab + '"]').parent().index(); } jQuery('.ticketTabs').tabs({ create: function ( event, ui ) { jQuery('.ticketTabs').css("visibility", "visible"); }, activate: function (event, ui) { url = new URL(window.location.href); url.searchParams.set('tab', ui.newPanel[0].id); window.history.replaceState(null, null, url); }, load: function () { }, enable: function () { }, active: activeTabIndex }); }); }; var initTicketSearchSubmit = function (url) { jQuery("#ticketSearch").on('submit', function (e) { e.preventDefault(); var project = jQuery("#projectIdInput").val(); var users = jQuery("#userSelect").val(); var milestones = jQuery("#milestoneSelect").val(); var term = jQuery("#termInput").val(); var sprints = jQuery("#sprintSelect").val(); var types = jQuery("#typeSelect").val(); var priority = jQuery("#prioritySelect").val(); var status = jQuery("#statusSelect").val(); var projects = jQuery("#projectsSelect").val(); var sort = jQuery("#sortBySelect").val(); var groupBy = jQuery("input[name='groupBy']:checked").val(); var showTasks = jQuery("input[name='showTasks']:checked").val(); // Encode each value: these come from the DOM (filter inputs) and are concatenated // into a URL, so encodeURIComponent keeps special characters (&, #, spaces, commas) // from breaking the query and closes the DOM-text-to-navigation sink. var query = "?search=true"; if (project != "" && project != undefined) { query = query + "&projectId=" + encodeURIComponent(project)} if (users != "" && users != undefined) { query = query + "&users=" + encodeURIComponent(users)} if (milestones != "" && milestones != undefined) { query = query + "&milestone=" + encodeURIComponent(milestones)} if (term != "" && term != undefined) { query = query + "&term=" + encodeURIComponent(term);} if (sprints != "" && sprints != undefined) { query = query + "&sprint=" + encodeURIComponent(sprints);} if (types != "" && types != undefined) { query = query + "&type=" + encodeURIComponent(types);} if (priority != "" && priority != undefined) { query = query + "&priority=" + encodeURIComponent(priority);} if (status != "" && status != undefined) { query = query + "&status=" + encodeURIComponent(status);} if (projects != "" && projects != undefined) { query = query + "&projects=" + encodeURIComponent(projects);} if (sort != "" && sort != undefined) { query = query + "&sort=" + encodeURIComponent(sort);} if (groupBy != "" && groupBy != undefined) { query = query + "&groupBy=" + encodeURIComponent(groupBy);} if (showTasks != "" && showTasks != undefined) { query = query + "&showTasks=" + encodeURIComponent(showTasks);} var rediredirectUrl = url + query; window.location.href = rediredirectUrl; }); }; var initTicketSearchUrlBuilder = function (url) { var project = jQuery("#projectIdInput").val(); var users = jQuery("#userSelect").val(); var milestones = jQuery("#milestoneSelect").val(); var term = jQuery("#termInput").val(); var sprints = jQuery("#sprintSelect").val(); var types = jQuery("#typeSelect").val(); var priority = jQuery("#prioritySelect").val(); var status = jQuery("#statusSelect").val(); var projects = jQuery("#projectsSelect").val(); var sort = jQuery("#sortBySelect").val(); var groupBy = jQuery("input[name='groupBy']:checked").val(); // Encode each value (DOM-sourced filter inputs concatenated into a URL) — see the // matching note in initTicketSearchSubmit. var query = "?search=true"; if (project != "" && project != undefined) { query = query + "&projectId=" + encodeURIComponent(project)} if (users != "" && users != undefined) { query = query + "&users=" + encodeURIComponent(users)} if (milestones != "" && milestones != undefined) { query = query + "&milestone=" + encodeURIComponent(milestones)} if (term != "" && term != undefined) { query = query + "&term=" + encodeURIComponent(term);} if (sprints != "" && sprints != undefined) { query = query + "&sprint=" + encodeURIComponent(sprints);} if (types != "" && types != undefined) { query = query + "&type=" + encodeURIComponent(types);} if (priority != "" && priority != undefined) { query = query + "&priority=" + encodeURIComponent(priority);} if (status != "" && status != undefined) { query = query + "&status=" + encodeURIComponent(status);} if (projects != "" && projects != undefined) { query = query + "&projects=" + encodeURIComponent(projects);} if (sort != "" && sort != undefined) { query = query + "&sort=" + encodeURIComponent(sort);} if (groupBy != "" && groupBy != undefined) { query = query + "&groupBy=" + encodeURIComponent(groupBy);} var rediredirectUrl = url + query; window.location.href = rediredirectUrl; }; var setUpKanbanColumns = function () { jQuery(document).ready(function () { countTickets(); updateSwimlaneCounts(); jQuery(".filterBar .row-fluid").css("opacity", "1"); jQuery(".sortableTicketList").each(function(){ let kanbanLaneId = jQuery(this).attr("id"); // Skip collapsed swimlanes - let CSS handle their height var swimlaneRow = jQuery(this).closest('.kanban-swimlane-row'); if (swimlaneRow.length && swimlaneRow.attr('data-expanded') === 'false') { return; } let height = 250; jQuery(this).find(".column .contentInner").each(function () { if (jQuery(this).height() > height) { height = jQuery(this).height(); } }); jQuery("#"+kanbanLaneId+" .column .contentInner").css("height", height); }); }); } var initTicketKanban = function (ticketStatusListParameter) { var ticketStatusList = ticketStatusListParameter; jQuery(".sortableTicketList.kanbanBoard .ticketBox").hover(function () { jQuery(this).css("background", "var(--kanban-card-hover)"); },function () { jQuery(this).css("background", "var(--kanban-card-bg)"); }); var position_updated = false; jQuery(".sortableTicketList").each(function () { var currentElement = this; jQuery(currentElement).find(".contentInner").sortable({ connectWith: ".contentInner", items: "> .moveable", tolerance: 'intersect', placeholder: "ui-state-highlight", forcePlaceholderSize: true, cancel: ".portlet-toggle,:input,a,input", distance: 10, start: function (event, ui) { ui.item.addClass('tilt'); tilt_direction(ui.item); // Store original swimlane for cross-swimlane detection var $originalSwimlane = ui.item.closest('.sortableTicketList.kanbanBoard'); ui.item.data('originalSwimlaneId', $originalSwimlane.attr('id')); }, stop: function (event, ui) { ui.item.removeClass("tilt"); jQuery("html").unbind('mousemove', ui.item.data("move_handler")); ui.item.removeData("move_handler"); countTickets(); updateSwimlaneCounts(); // Update empty state for all columns after drag-and-drop jQuery(".sortableTicketList").find(".contentInner").each(function() { var $container = jQuery(this); var hasTickets = $container.find(".moveable").length > 0; if (hasTickets) { // Column has tickets - remove empty state $container.removeClass("empty-column"); $container.attr("data-empty-text", ""); var currentLabel = $container.attr("aria-label") || ""; $container.attr("aria-label", currentLabel.replace("Empty column", "") + " column items"); } else { // Column is empty - add empty state $container.addClass("empty-column"); $container.attr("data-empty-text", "Empty"); $container.attr("aria-label", "Empty column"); } }); // Detect cross-swimlane movement and update groupBy field var $newSwimlane = ui.item.closest('.sortableTicketList.kanbanBoard'); var newSwimlaneId = $newSwimlane.attr('id'); var originalSwimlaneId = ui.item.data('originalSwimlaneId'); if (originalSwimlaneId && newSwimlaneId && originalSwimlaneId !== newSwimlaneId) { // Card moved to different swimlane - update the groupBy field var ticketId = ui.item[0].id.replace('ticket_', ''); var newGroupValue = newSwimlaneId.replace('kanboard-', ''); // Map groupBy values to API field names var groupByFieldMap = { 'milestoneid': 'milestoneid', 'editorId': 'editorId', 'priority': 'priority', 'storypoints': 'storypoints', 'sprint': 'sprint', 'dueDate': 'dateToFinish' }; var groupBy = leantime.kanbanGroupBy; var fieldName = groupByFieldMap[groupBy]; // Special handling for dueDate - calculate actual date from bucket ID if (groupBy === 'dueDate') { var today = new Date(); today.setHours(12, 0, 0, 0); // Set to noon to avoid timezone issues // Bucket IDs: 0=overdue, 1=due-this-week, 2=due-next-week, 3=due-later, 4=no-due-date switch (newGroupValue) { case '0': // Overdue - set to yesterday var yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); newGroupValue = yesterday.toISOString().split('T')[0]; break; case '1': // Due This Week - set to end of this week (Saturday) var endOfWeek = new Date(today); var daysUntilSaturday = 6 - endOfWeek.getDay(); if (daysUntilSaturday < 0) daysUntilSaturday = 0; endOfWeek.setDate(endOfWeek.getDate() + daysUntilSaturday); newGroupValue = endOfWeek.toISOString().split('T')[0]; break; case '2': // Due Next Week - set to next week Saturday var nextWeekEnd = new Date(today); var daysUntilNextSaturday = 6 - nextWeekEnd.getDay() + 7; nextWeekEnd.setDate(nextWeekEnd.getDate() + daysUntilNextSaturday); newGroupValue = nextWeekEnd.toISOString().split('T')[0]; break; case '3': // Due Later - set to 3 weeks from now var later = new Date(today); later.setDate(later.getDate() + 21); newGroupValue = later.toISOString().split('T')[0]; break; case '4': // No Due Date - clear the date newGroupValue = ''; break; } } if (fieldName && ticketId) { var $card = jQuery('#ticket_' + ticketId); // OPTIMISTIC UI UPDATE - update visuals immediately before server confirms if (groupBy === 'milestoneid') { // Find the new swimlane's label for the milestone var $newSwimlaneHeader = jQuery('#swimlane-row-' + newGroupValue + ' .swimlane-header-label'); var newLabel = $newSwimlaneHeader.text() || leantime.i18n.__("label.no_milestone"); // Update milestone dropdown on card var $milestoneDropdown = $card.find('.milestoneDropdown .dropdown-toggle .text'); if ($milestoneDropdown.length) { $milestoneDropdown.text(newLabel); } // Get the milestone color from the dropdown menu item var $milestoneMenuItem = $card.find('.milestoneDropdown .dropdown-menu a[data-value^="' + ticketId + '_' + newGroupValue + '_"]'); var milestoneColor = '#b0b0b0'; // Default gray for "No Milestone" if ($milestoneMenuItem.length && newGroupValue !== '0' && newGroupValue !== '') { // Extract color from the data-value (format: ticketId_milestoneId_color) var dataValue = $milestoneMenuItem.attr('data-value'); if (dataValue) { var parts = dataValue.split('_'); if (parts.length >= 3) { milestoneColor = parts.slice(2).join('_'); // Handle colors with underscores } } // Also try getting from inline style if (!milestoneColor || milestoneColor === '#b0b0b0') { var inlineStyle = $milestoneMenuItem.attr('style'); if (inlineStyle) { var colorMatch = inlineStyle.match(/background-color:\s*([^;]+)/i); if (colorMatch) { milestoneColor = colorMatch[1].trim(); } } } } // Update the dropdown toggle background color $card.find('.milestoneDropdown .dropdown-toggle').css('background-color', milestoneColor); } else if (groupBy === 'priority') { // Update priority border and dropdown on card $card.removeClass(function(index, className) { return (className.match(/(^|\s)priority-border-\S+/g) || []).join(' '); }).addClass('priority-border-' + newGroupValue); // Update priority dropdown text and background var priorityLabels = {'1': 'Critical', '2': 'High', '3': 'Medium', '4': 'Low', '5': 'Lowest'}; var $priorityDropdown = jQuery('#priorityDropdownMenuLink' + ticketId); if ($priorityDropdown.length) { $priorityDropdown.find('span.text').text(priorityLabels[newGroupValue] || newGroupValue); $priorityDropdown.removeClass('priority-bg-1 priority-bg-2 priority-bg-3 priority-bg-4 priority-bg-5'); $priorityDropdown.addClass('priority-bg-' + newGroupValue); } } else if (groupBy === 'editorId') { // Update user dropdown on card var $userDropdown = jQuery('#userDropdownMenuLink' + ticketId); if ($userDropdown.length) { // Update user image $userDropdown.find('span.text span img').attr('src', leantime.appUrl + '/users/profileImage/' + newGroupValue); // Get the new user's name from swimlane header var $newSwimlaneHeader = jQuery('#swimlane-row-' + newGroupValue + ' .swimlane-header-label'); var newUserName = $newSwimlaneHeader.text() || leantime.i18n.__("label.not_assigned"); $userDropdown.find('span.text span#user' + ticketId).text(newUserName); } } else if (groupBy === 'storypoints') { // Update effort dropdown on card var storyPointLabels = {'0.5': '< 2min', '1': 'XS', '2': 'S', '3': 'M', '5': 'L', '8': 'XL', '13': 'XXL'}; var $effortDropdown = jQuery('#effortDropdownMenuLink' + ticketId); if ($effortDropdown.length) { $effortDropdown.find('span.text').text(storyPointLabels[newGroupValue] || newGroupValue); } } else if (groupBy === 'sprint') { // Update sprint dropdown on card var $sprintDropdown = jQuery('#sprintDropdownMenuLink' + ticketId); if ($sprintDropdown.length) { // Get the new sprint's name from swimlane header var $newSwimlaneHeader = jQuery('#swimlane-row-' + newGroupValue + ' .swimlane-header-label'); var newSprintName = $newSwimlaneHeader.text() || leantime.i18n.__("label.backlog"); $sprintDropdown.find('span.text').text(newSprintName); } } else if (groupBy === 'dueDate') { // Update due date display on card var $dateDisplay = $card.find('.dues .fa-calendar').parent(); if ($dateDisplay.length && newGroupValue) { // Format the date for display (MM/DD/YYYY) var dateParts = newGroupValue.split('-'); var formattedDate = dateParts[1] + '/' + dateParts[2] + '/' + dateParts[0]; $dateDisplay.contents().filter(function() { return this.nodeType === 3; // Text nodes only }).first().replaceWith(' ' + formattedDate); } else if ($dateDisplay.length && !newGroupValue) { // Clear the date display for "No Due Date" $dateDisplay.contents().filter(function() { return this.nodeType === 3; }).first().replaceWith(' No due date'); } } // Show success notification immediately for better perceived speed jQuery.growl({message: "To-Do Updated", style: "success"}); var swimlaneValues = {}; swimlaneValues[fieldName] = newGroupValue; // PATCH the ticket with the new swimlane value (confirmation happens in background) leantime.rpc('Tickets.Tickets.patchTicket', { id: ticketId, values: swimlaneValues }) .catch(function() { jQuery.growl({message: leantime.i18n.__("short_notifications.not_saved") || "Error updating ticket", style: "error"}); // Reload on failure to restore correct state location.reload(); }); } } // Clean up stored data ui.item.removeData('originalSwimlaneId'); // Get the new parent swimlane for status update var $targetSwimlane = ui.item.closest('.sortableTicketList'); var sortPayload = {}; var sortHandler = ui.item[0].id; for (var i = 0; i < ticketStatusList.length; i++) { if ($targetSwimlane.find(".contentInner.status_" + ticketStatusList[i]).length) { sortPayload[ticketStatusList[i]] = $targetSwimlane.find(".contentInner.status_" + ticketStatusList[i]).sortable('serialize'); } } leantime.rpc('Tickets.Tickets.updateTicketStatusAndSorting', { params: sortPayload, handler: sortHandler }) .catch(function (error) { console.error('Could not update ticket status and sorting', error); }); } }); }); function tilt_direction(item) { var left_pos = item.position().left, move_handler = function (e) { if ((e.pageX + 5) > left_pos) { item.addClass("right"); item.removeClass("left"); } else if (e.pageX < (left_pos + 5)) { item.addClass("left"); item.removeClass("right"); } else { item.removeClass("left"); item.removeClass("right"); } left_pos = e.pageX; }; jQuery("html").bind("mousemove", move_handler); item.data("move_handler", move_handler); } jQuery(".portlet") .addClass("ui-widget ui-widget-content ui-helper-clearfix ui-corner-all") .find(".portlet-header") .addClass("ui-widget-header ui-corner-all") .prepend(""); jQuery(".portlet-toggle").click(function () { var icon = jQuery(this); icon.toggleClass("ui-icon-minusthick ui-icon-plusthick"); icon.closest(".portlet").find(".portlet-content").toggle(); }); }; var initTicketsTable = function (groupBy) { function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } jQuery(document).ready(function () { var size = 100; var columnIndex = false; var defaultOrder = []; var allTickets = jQuery(".ticketTable").DataTable({ "language": { "decimal": leantime.i18n.__("datatables.decimal"), "emptyTable": leantime.i18n.__("datatables.emptyTable"), "info": leantime.i18n.__("datatables.info"), "infoEmpty": leantime.i18n.__("datatables.infoEmpty"), "infoFiltered": leantime.i18n.__("datatables.infoFiltered"), "infoPostFix": leantime.i18n.__("datatables.infoPostFix"), "thousands": leantime.i18n.__("datatables.thousands"), "lengthMenu": leantime.i18n.__("datatables.lengthMenu"), "loadingRecords": leantime.i18n.__("datatables.loadingRecords"), "processing": leantime.i18n.__("datatables.processing"), "search": leantime.i18n.__("datatables.search"), "zeroRecords": leantime.i18n.__("datatables.zeroRecords"), "paginate": { "first": leantime.i18n.__("datatables.first"), "last": leantime.i18n.__("datatables.last"), "next": leantime.i18n.__("datatables.next"), "previous": leantime.i18n.__("datatables.previous"), }, "aria": { "sortAscending": leantime.i18n.__("datatables.sortAscending"), "sortDescending":leantime.i18n.__("datatables.sortDescending"), }, "buttons": { colvis: leantime.i18n.__("datatables.buttons.colvis"), csv: leantime.i18n.__("datatables.buttons.download") } }, "dom": '<"top">rt<"bottom">p<"clear">', "searching": false, "stateSave": true, "displayLength":100, "order": defaultOrder, "columnDefs": [ { "visible": false, "targets": 10 }, { "visible": false, "targets": 11 }, { "target": "no-sort", "orderable": false}, ], "footerCallback": function ( row, data, start, end, display ) { var api = this.api(), data; // converting to interger to find total var intVal = function ( i ) { return typeof i === 'string' ? i.replace(/[\$,]/g, '') * 1 : typeof i === 'number' ? i : 0; }; // computing column Total of the complete result var plannedHours = api .column(10) .data() .reduce(function (a, b) { if (isNumeric(a) === false) { a = jQuery(a).val(); } if (isNumeric(b) === false) { b = jQuery(b).val(); } if (isNaN(a)) { a = 0; } if (isNaN(b)) { b = 0; } return parseFloat(a) + parseFloat(b); }, 0); var hoursLeft = api .column(11) .data() .reduce(function (a, b) { if (isNumeric(a) === false) { a = jQuery(a).val(); } if (isNumeric(b) === false) { b = jQuery(b).val(); } if (isNaN(a)) { a = 0; } if (isNaN(b)) { b = 0; } return parseFloat(a) + parseFloat(b); }, 0); var loggedHours = api .column(12) .data() .reduce(function (a, b) { return parseFloat(a) + parseFloat(b); }, 0); // Update footer by showing the total with the reference of the column index jQuery(api.column(9).footer()).html('Total'); jQuery(api.column(10).footer()).html(plannedHours); jQuery(api.column(11).footer()).html(hoursLeft); jQuery(api.column(12).footer()).html(loggedHours); }, }); var buttons = new jQuery.fn.dataTable.Buttons(allTickets.table(0), { buttons: [ { extend: 'csvHtml5', title: leantime.i18n.__("label.filename_fileexport"), charset: 'utf-8', bom: true, exportOptions: { format: { body: function ( data, row, column, node ) { if ( typeof jQuery(node).data('order') !== 'undefined') { data = jQuery(node).data('order'); } return data; } } } }, { extend: 'colvis', columns: ':not(.noVis)' } ] }).container().appendTo(jQuery('#tableButtons')); // When the column visibility changes on the firs table, also change it on // the others tables. allTickets.table(0).on( 'column-visibility', function ( e, settings, colIdx, visibility ) { // Toggle the visibility for (var i = 1; i < allTickets.tables().context.length; i++) { allTickets.tables(i).column(colIdx).visible(visibility); } allTickets.draw(); } ); jQuery('.ticketTable input').on('change', function ( e, settings, column, state ) { jQuery(this).parent().attr('data-order',jQuery(this).val()); allTickets.draw(); }); }); }; var initTicketsList = function (groupBy) { jQuery(document).ready(function () { var size = 50; var columnIndex = false; var collapsedGroups = {}; var defaultOrder = []; var allTickets = jQuery(".listStyleTable").DataTable({ "language": { "decimal": leantime.i18n.__("datatables.decimal"), "emptyTable": leantime.i18n.__("datatables.emptyTable"), "info": leantime.i18n.__("datatables.info"), "infoEmpty": leantime.i18n.__("datatables.infoEmpty"), "infoFiltered": leantime.i18n.__("datatables.infoFiltered"), "infoPostFix": leantime.i18n.__("datatables.infoPostFix"), "thousands": leantime.i18n.__("datatables.thousands"), "lengthMenu": leantime.i18n.__("datatables.lengthMenu"), "loadingRecords": leantime.i18n.__("datatables.loadingRecords"), "processing": leantime.i18n.__("datatables.processing"), "search": leantime.i18n.__("datatables.search"), "zeroRecords": leantime.i18n.__("datatables.zeroRecords"), "paginate": { "first": leantime.i18n.__("datatables.first"), "last": leantime.i18n.__("datatables.last"), "next": leantime.i18n.__("datatables.next"), "previous": leantime.i18n.__("datatables.previous"), }, "aria": { "sortAscending": leantime.i18n.__("datatables.sortAscending"), "sortDescending":leantime.i18n.__("datatables.sortDescending"), }, "buttons": { colvis: leantime.i18n.__("datatables.buttons.colvis"), csv: leantime.i18n.__("datatables.buttons.download") } }, "dom": '<"top">rt<"bottom"<"center"p>><"clear">', "searching": false, "stateSave": true, "displayLength":25, "order": defaultOrder, "fnDrawCallback": function (oSettings) { if (oSettings._iDisplayLength > oSettings.fnRecordsDisplay()) { jQuery(oSettings.nTableWrapper).find('.dataTables_paginate').hide(); } else { jQuery(oSettings.nTableWrapper).find('.dataTables_paginate').show(); } } }); }); }; var initMilestoneTable = function (groupBy) { function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } jQuery(document).ready(function () { var size = 100; var columnIndex = false; var defaultOrder = []; var allTickets = jQuery(".ticketTable").DataTable({ "language": { "decimal": leantime.i18n.__("datatables.decimal"), "emptyTable": leantime.i18n.__("datatables.emptyTable"), "info": leantime.i18n.__("datatables.info"), "infoEmpty": leantime.i18n.__("datatables.infoEmpty"), "infoFiltered": leantime.i18n.__("datatables.infoFiltered"), "infoPostFix": leantime.i18n.__("datatables.infoPostFix"), "thousands": leantime.i18n.__("datatables.thousands"), "lengthMenu": leantime.i18n.__("datatables.lengthMenu"), "loadingRecords": leantime.i18n.__("datatables.loadingRecords"), "processing": leantime.i18n.__("datatables.processing"), "search": leantime.i18n.__("datatables.search"), "zeroRecords": leantime.i18n.__("datatables.zeroRecords"), "paginate": { "first": leantime.i18n.__("datatables.first"), "last": leantime.i18n.__("datatables.last"), "next": leantime.i18n.__("datatables.next"), "previous": leantime.i18n.__("datatables.previous"), }, "aria": { "sortAscending": leantime.i18n.__("datatables.sortAscending"), "sortDescending":leantime.i18n.__("datatables.sortDescending"), }, "buttons": { colvis: leantime.i18n.__("datatables.buttons.colvis"), csv: leantime.i18n.__("datatables.buttons.download") } }, "dom": '<"top">rt<"bottom"><"clear">', "searching": false, "stateSave": true, "displayLength":100, "order": defaultOrder, "columnDefs": [ { "visible": false, "targets": 7 }, { "visible": false, "targets": 8 }, { "target": "no-sort", "orderable": false}, ] }); var buttons = new jQuery.fn.dataTable.Buttons(allTickets.table(0), { buttons: [ { extend: 'csvHtml5', title: leantime.i18n.__("label.filename_fileexport"), charset: 'utf-8', bom: true, exportOptions: { format: { body: function ( data, row, column, node ) { if ( typeof jQuery(node).data('order') !== 'undefined') { data = jQuery(node).data('order'); } return data; } } } }, { extend: 'colvis', columns: ':not(.noVis)' } ] }).container().appendTo(jQuery('#tableButtons')); // When the column visibility changes on the firs table, also change it on // the others tables. allTickets.table(0).on( 'column-visibility', function ( e, settings, colIdx, visibility ) { // Toggle the visibility for (var i = 1; i < allTickets.tables().context.length; i++) { allTickets.tables(i).column(colIdx).visible(visibility); } allTickets.draw(); } ); jQuery('.ticketTable input').on('change', function ( e, settings, column, state ) { jQuery(this).parent().attr('data-order',jQuery(this).val()); allTickets.draw(); }); }); }; var loadTicketToContainer = function (id, element) { // Destroy any Tiptap editors before loading new content if (window.leantime && window.leantime.tiptapController && window.leantime.tiptapController.registry) { leantime.tiptapController.registry.destroyAll(); } jQuery(".ticketRows").removeClass("active"); jQuery("#row-" + id).addClass("active"); jQuery(element).html("
"); function formSubmitHandler(element) { jQuery(element).find("form").each(function () { jQuery(this).on("submit", function (e) { e.preventDefault(); // Destroy any Tiptap editors before submitting if (window.leantime && window.leantime.tiptapController && window.leantime.tiptapController.registry) { leantime.tiptapController.registry.destroyAll(); } jQuery(element).html("
"); var data = jQuery(this).serialize(); jQuery.ajax({ url: jQuery(this).attr("action"), data: data, type: "post", success: function (data) { jQuery(element).html(data); formSubmitHandler(element); }, error: function () { } }); }); }); } jQuery.get(leantime.appUrl + '/tickets/showTicket/' + id, function ( data ) { jQuery(element).html(data); formSubmitHandler(element); }); }; var initTagsInput = function ( ) { jQuery("#tags").tagsInput({ // autocomplete_url must be defined to enable the autocomplete branch in the tagsInput plugin. // The actual data source is overridden below via the jQuery UI autocomplete `source` function. 'autocomplete_url': true, 'autocomplete': { source: function (request, response) { leantime.rpc('Tags.Tags.getTags', { projectId: leantime.currentProject, term: request.term }) .then(function (tags) { response(tags || []); }) .catch(function (error) { console.error('Could not load tags', error); response([]); }); } } }); jQuery("#tags_tag").on("focusout", function () { let tag = jQuery(this).val(); if (tag != '') { jQuery("#tags").addTag(tag); } }); }; var addCommentTimesheetContent = function (commentId, taskId) { var content = "Discussion on To-Do #" + taskId + ":" + "\n\r" + jQuery("#commentText-" + commentId).text(); jQuery('li a[href*="timesheet"]').click(); jQuery("#timesheet #description").val(content); }; var openMilestoneModalManually = function (url) { jQuery.nmManual(url, { stack: true, sizes: { minW: 500, minH: 500 } }); }; var openTicketModalManually = function (url) { jQuery.nmManual(url, { sizes: { minW: 1800, minH: 1800 }, resizable: true, autoSizable: true, }); }; // Make public what you want to have public, everything else is private return { toggleFilterBar: toggleFilterBar, initGanttChart:initGanttChart, updateRemainingHours:updateRemainingHours, updatePlannedHours:updatePlannedHours, initTimeSheetChart:initTimeSheetChart, initTicketTabs:initTicketTabs, initTicketSearchSubmit:initTicketSearchSubmit, initTicketKanban:initTicketKanban, initTicketsTable:initTicketsTable, initEffortDropdown:initEffortDropdown, initPriorityDropdown:initPriorityDropdown, initMilestoneDropdown:initMilestoneDropdown, initStatusDropdown:initStatusDropdown, initUserDropdown:initUserDropdown, initSprintDropdown:initSprintDropdown, initToolTips:initToolTips, initTagsInput:initTagsInput, initMilestoneDatesAsyncUpdate:initMilestoneDatesAsyncUpdate, initAsyncInputChange:initAsyncInputChange, initDueDateTimePickers:initDueDateTimePickers, setUpKanbanColumns:setUpKanbanColumns, addCommentTimesheetContent:addCommentTimesheetContent, initMilestoneTable:initMilestoneTable, initMilestoneDates:_initMilestoneDates, initTicketsList:initTicketsList, loadTicketToContainer:loadTicketToContainer, initTicketSearchUrlBuilder:initTicketSearchUrlBuilder, initSprintDates:initSprintDates, initSimpleColorPicker:initSimpleColorPicker, openMilestoneModalManually:openMilestoneModalManually, openTicketModalManually:openTicketModalManually }; })();