From ee3c20c6316ea942f8bd4ac74e40657e50a714d3 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Mon, 23 Feb 2026 15:03:05 +0100 Subject: [PATCH 01/24] [#6002] Add listener for nyroModal and wait it to be initialized --- app/Domain/Timesheets/Js/timesheetsController.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Domain/Timesheets/Js/timesheetsController.js b/app/Domain/Timesheets/Js/timesheetsController.js index 5c8d2085cc..7f8cd0c26d 100644 --- a/app/Domain/Timesheets/Js/timesheetsController.js +++ b/app/Domain/Timesheets/Js/timesheetsController.js @@ -159,11 +159,11 @@ var initEditTimeModal = function () { titleFromIframe: true }; - jQuery(document).on('click', 'a.editTimeModal', function (e) { - if (!isReady) return; - e.preventDefault(); - jQuery(this).nyroModal(canvasoptions); - }); + jQuery(document).on('click.nyroInit', 'a.editTimeModal', function (e) { + e.preventDefault(); + jQuery(this).nyroModal(canvasoptions); + jQuery(this).trigger('click.nyroModal'); + }); }; initWhenReady(); From c4f544b5d67ece5fe3a1e234007c3bfe99a0e7dd Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Mon, 23 Feb 2026 17:05:37 +0100 Subject: [PATCH 02/24] #6003 Save old filters after navigating in company page --- .../Timesheets/Templates/editTime.tpl.php | 10 ++++ .../Timesheets/Templates/showAll.tpl.php | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/app/Domain/Timesheets/Templates/editTime.tpl.php b/app/Domain/Timesheets/Templates/editTime.tpl.php index b79620e56e..4bc7aa32e9 100644 --- a/app/Domain/Timesheets/Templates/editTime.tpl.php +++ b/app/Domain/Timesheets/Templates/editTime.tpl.php @@ -60,6 +60,16 @@ jQuery.growl({ message: "Could not save time entry", style: "error" }); }); }); + jQuery(document).ready(function() { + jQuery('a.delete.editTimeModal').on('click', function(e) { + e.preventDefault(); + var deleteUrl = jQuery(this).attr('href'); + var parentParams = ''; + if (window.parent && window.parent.location) { + parentParams = window.parent.location.search; + } + window.parent.location.href = deleteUrl + (parentParams ? '?' + parentParams.substring(1) : ''); + }); }); diff --git a/app/Domain/Timesheets/Templates/showAll.tpl.php b/app/Domain/Timesheets/Templates/showAll.tpl.php index 03e85ff174..053f9f682e 100644 --- a/app/Domain/Timesheets/Templates/showAll.tpl.php +++ b/app/Domain/Timesheets/Templates/showAll.tpl.php @@ -73,6 +73,59 @@ function updateProjectCountInline() { } jQuery(document).ready(function(){ + function saveFilterState() { + var formData = jQuery('#form').serialize(); + localStorage.setItem('timesheetFilterState', formData); + } + + jQuery('#form').on('submit', saveFilterState); + jQuery('#form select, #form input[type="checkbox"]').on('change', saveFilterState); + document.getElementById('form').addEventListener('submit', saveFilterState, true); + +function restoreFilters() { + var savedState = localStorage.getItem('timesheetFilterState'); + if (!savedState) return; + + var params = new URLSearchParams(savedState); + + jQuery('#form input[type="checkbox"]').prop('checked', false); + + params.forEach(function(value, key) { + var safeKey = key.replace(/([\[\]])/g, '\\$1'); + var fields = jQuery('#form [name="' + safeKey + '"]'); + if (fields.length && fields.first().is(':checkbox')) { + fields.each(function() { + if (this.value === value) this.checked = true; + }); + } else { + fields.val(value); + } + }); + + jQuery('#allTimesheetsTable tbody').css('opacity', '0.3'); + jQuery('#allTimesheetsTable tbody').css('pointer-events', 'none'); + + jQuery('#form').submit(); + } + + var _wentToTicket = false; + + jQuery(window).on('hashchange', function() { + var hash = window.location.hash; + + if (hash.indexOf('/tickets/showTicket/') !== -1) { + _wentToTicket = true; + saveFilterState(); + } else if (_wentToTicket) { + _wentToTicket = false; + restoreFilters(); + } + }); + + var isPostBack = ; + if (!isPostBack) { + restoreFilters(); + } jQuery("#checkAllEmpl").change(function(){ jQuery(".invoicedEmpl").prop('checked', jQuery(this).prop("checked")); if (jQuery(this).prop("checked") == true) { From 091f676066703f658c11417d70b03b46c5dc6ef6 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 24 Feb 2026 10:29:22 +0100 Subject: [PATCH 03/24] #6003 Add listener for routes on which user navigates, so filters are being reset to default after navigating --- app/Domain/Timesheets/Templates/showAll.tpl.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Domain/Timesheets/Templates/showAll.tpl.php b/app/Domain/Timesheets/Templates/showAll.tpl.php index 053f9f682e..65653cee6b 100644 --- a/app/Domain/Timesheets/Templates/showAll.tpl.php +++ b/app/Domain/Timesheets/Templates/showAll.tpl.php @@ -78,7 +78,13 @@ function saveFilterState() { localStorage.setItem('timesheetFilterState', formData); } - jQuery('#form').on('submit', saveFilterState); + jQuery(document).on('click', 'a[href]', function() { + var href = jQuery(this).attr('href'); + if (!href || href.indexOf('#') === 0) return; + if (href.indexOf('timesheets') === -1) { + localStorage.removeItem('timesheetFilterState'); + } + }); jQuery('#form select, #form input[type="checkbox"]').on('change', saveFilterState); document.getElementById('form').addEventListener('submit', saveFilterState, true); From 8660b11718138ee3963a4bede5bac25138e27a56 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 24 Feb 2026 14:57:45 +0100 Subject: [PATCH 04/24] #5984 Add global css font colors --- app/Core/UI/Theme.php | 6 +++--- app/Views/Templates/layouts/app.blade.php | 2 +- public/assets/js/app/core/snippets.js | 22 +++++++++------------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/Core/UI/Theme.php b/app/Core/UI/Theme.php index 9c63fbc81f..9f365f7dee 100644 --- a/app/Core/UI/Theme.php +++ b/app/Core/UI/Theme.php @@ -119,12 +119,12 @@ class Theme 'grayscale1' => [ 'name' => 'Grayscale', 'primaryColor' => '#000000', - 'secondaryColor' => '#757575', + 'secondaryColor' => '#ffffff', ], 'grayscale2' => [ 'name' => 'Grayscale Reverse', - 'primaryColor' => '#d1d1d1', - 'secondaryColor' => '#000000', + 'primaryColor' => '#000000', + 'secondaryColor' => '#ffffff', ], 'themeDefault' => 'themeDefault', 'companyColors' => 'companyColors', diff --git a/app/Views/Templates/layouts/app.blade.php b/app/Views/Templates/layouts/app.blade.php index 6c642d9408..ac539debec 100644 --- a/app/Views/Templates/layouts/app.blade.php +++ b/app/Views/Templates/layouts/app.blade.php @@ -5,7 +5,7 @@ @stack('styles') - + @include('global::sections.appAnnouncement') diff --git a/public/assets/js/app/core/snippets.js b/public/assets/js/app/core/snippets.js index 81c2a34415..96408fed4e 100644 --- a/public/assets/js/app/core/snippets.js +++ b/public/assets/js/app/core/snippets.js @@ -64,19 +64,15 @@ leantime.snippets = (function () { }; - var toggleTheme = function (theme) { - - var themeUrl = jQuery("#themeStyleSheet").attr("href"); - - if(theme == "light"){ - themeUrl = themeUrl.replace("dark.css", "light.css"); - jQuery("#themeStyleSheet").attr("href", themeUrl); - }else if (theme == "dark"){ - themeUrl = themeUrl.replace("light.css", "dark.css"); - jQuery("#themeStyleSheet").attr("href", themeUrl); - } - - }; +var toggleTheme = function (theme) { + var themeUrl = jQuery("#themeStyleSheet").attr("href"); + themeUrl = themeUrl.replace(/light\.css|dark\.css/, theme + ".css"); + jQuery("#themeStyleSheet").attr("href", themeUrl); + + jQuery("body") + .removeClass("colorMode-light colorMode-dark") + .addClass("colorMode-" + theme); +}; var toggleBg = function (theme) { From f4588b38ff582343181e739eef61ba843a2fd565 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 24 Feb 2026 17:29:32 +0100 Subject: [PATCH 05/24] #5984 Add appropriate colors in leantime scheme --- app/Core/UI/Theme.php | 6 +++--- app/Domain/Users/Templates/editOwn.blade.php | 2 +- public/assets/js/app/core/snippets.js | 15 +++++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/Core/UI/Theme.php b/app/Core/UI/Theme.php index 9f365f7dee..9c63fbc81f 100644 --- a/app/Core/UI/Theme.php +++ b/app/Core/UI/Theme.php @@ -119,12 +119,12 @@ class Theme 'grayscale1' => [ 'name' => 'Grayscale', 'primaryColor' => '#000000', - 'secondaryColor' => '#ffffff', + 'secondaryColor' => '#757575', ], 'grayscale2' => [ 'name' => 'Grayscale Reverse', - 'primaryColor' => '#000000', - 'secondaryColor' => '#ffffff', + 'primaryColor' => '#d1d1d1', + 'secondaryColor' => '#000000', ], 'themeDefault' => 'themeDefault', 'companyColors' => 'companyColors', diff --git a/app/Domain/Users/Templates/editOwn.blade.php b/app/Domain/Users/Templates/editOwn.blade.php index cedd5fef36..44bc55ecc0 100644 --- a/app/Domain/Users/Templates/editOwn.blade.php +++ b/app/Domain/Users/Templates/editOwn.blade.php @@ -354,7 +354,7 @@
@foreach($availableColorSchemes as $key => $scheme ) - + diff --git a/public/assets/js/app/core/snippets.js b/public/assets/js/app/core/snippets.js index 96408fed4e..21aac571e3 100644 --- a/public/assets/js/app/core/snippets.js +++ b/public/assets/js/app/core/snippets.js @@ -95,12 +95,15 @@ var toggleTheme = function (theme) { }; - var toggleColors = function (accent1, accent2) { - - jQuery("#colorSchemeSetter").html(":root { --accent1: "+accent1+"; --accent2: "+accent2+"}") - - - }; +var toggleColors = function (accent1, accent2, schemeKey) { + jQuery("#colorSchemeSetter").html(":root { --accent1: " + accent1 + "; --accent2: " + accent2 + "}"); + + if (schemeKey) { + jQuery("body").removeClass(function(i, cls) { + return (cls.match(/\bcolorScheme-\S+/g) || []).join(' '); + }).addClass("colorScheme-" + schemeKey); + } +}; From 4cd39ccebc8258f7356fe1c38b316cf51bc2021a Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 25 Feb 2026 09:14:47 +0100 Subject: [PATCH 06/24] #5984 Change font color on dark theme default to white --- public/theme/default/css/dark.css | 2 +- public/theme/default/css/light.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/theme/default/css/dark.css b/public/theme/default/css/dark.css index 87218d2f54..ab130a2973 100644 --- a/public/theme/default/css/dark.css +++ b/public/theme/default/css/dark.css @@ -7,7 +7,7 @@ --accent2: hsla(168, 100%, 33%, 1); --accent2-hsl: 168, 100%, 33%; - --accent2-hover: #555; + --accent2-hover: #989898; --accent2-color: #fff; --nav-link-color: #fff; diff --git a/public/theme/default/css/light.css b/public/theme/default/css/light.css index 69fe613cbb..8433dcdd38 100644 --- a/public/theme/default/css/light.css +++ b/public/theme/default/css/light.css @@ -2,12 +2,12 @@ --accent1: hsla(199, 100%, 20%, 1); --accent1-hsl: 199, 100%, 20%; - --accent1-hover: #555; + --accent1-hover: #989898; --accent1-color:#fff; --accent2: hsla(168, 100%, 33%, 1); --accent2-hsl: 168, 100%, 33%; - --accent2-hover: #555; + --accent2-hover: #989898; --accent2-color:#fff; --nav-link-color: #fff; From dcda059049b2fbd7f063a29b40fd18446da27360 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 25 Feb 2026 09:38:37 +0100 Subject: [PATCH 07/24] #5984 Remove custom css from gitignore --- .gitignore | 1 - public/theme/default/css/custom.css | 117 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 public/theme/default/css/custom.css diff --git a/.gitignore b/.gitignore index a9fc388eac..7aba974d53 100644 --- a/.gitignore +++ b/.gitignore @@ -110,7 +110,6 @@ logs/* app/custom/domain/ app/custom/plugin/ app/custom/language/ -public/theme/*/css/custom.css public/theme/dts/ nbproject config/config.yaml diff --git a/public/theme/default/css/custom.css b/public/theme/default/css/custom.css new file mode 100644 index 0000000000..921f4502d1 --- /dev/null +++ b/public/theme/default/css/custom.css @@ -0,0 +1,117 @@ +body.colorScheme-grayscale1.colorMode-light, +body.colorScheme-grayscale1.colorMode-light h2, +body.colorScheme-grayscale1.colorMode-light h3, +body.colorScheme-grayscale1.colorMode-light h4, +body.colorScheme-grayscale1.colorMode-light h5, +body.colorScheme-grayscale1.colorMode-light h6, +body.colorScheme-grayscale1.colorMode-light p, +body.colorScheme-grayscale1.colorMode-light td, +body.colorScheme-grayscale1.colorMode-light th, +body.colorScheme-grayscale1.colorMode-light label, +body.colorScheme-grayscale1.colorMode-light select, +body.colorScheme-grayscale1.colorMode-light textarea, +body.colorScheme-grayscale1.colorMode-light .selectableContent { + color: black !important; +} + +body.colorScheme-grayscale1.colorMode-dark, +body.colorScheme-grayscale1.colorMode-dark h1, +body.colorScheme-grayscale1.colorMode-dark h2, +body.colorScheme-grayscale1.colorMode-dark h3, +body.colorScheme-grayscale1.colorMode-dark h4, +body.colorScheme-grayscale1.colorMode-dark h5, +body.colorScheme-grayscale1.colorMode-dark h6, +body.colorScheme-grayscale1.colorMode-dark p, +body.colorScheme-grayscale1.colorMode-dark span, +body.colorScheme-grayscale1.colorMode-dark td, +body.colorScheme-grayscale1.colorMode-dark th, +body.colorScheme-grayscale1.colorMode-dark label, +body.colorScheme-grayscale1.colorMode-dark a, +body.colorScheme-grayscale1.colorMode-dark li, +body.colorScheme-grayscale1.colorMode-dark input, +body.colorScheme-grayscale1.colorMode-dark select, +body.colorScheme-grayscale1.colorMode-dark textarea, +body.colorScheme-grayscale1.colorMode-dark button, +body.colorScheme-grayscale1.colorMode-dark .selectableContent { + color: white !important; +} + +body.colorScheme-grayscale2.colorMode-light, +body.colorScheme-grayscale2.colorMode-light h1, +body.colorScheme-grayscale2.colorMode-light h2, +body.colorScheme-grayscale2.colorMode-light h3, +body.colorScheme-grayscale2.colorMode-light h4, +body.colorScheme-grayscale2.colorMode-light h5, +body.colorScheme-grayscale2.colorMode-light h6, +body.colorScheme-grayscale2.colorMode-light p, +body.colorScheme-grayscale2.colorMode-light span, +body.colorScheme-grayscale2.colorMode-light td, +body.colorScheme-grayscale2.colorMode-light th, +body.colorScheme-grayscale2.colorMode-light label, +body.colorScheme-grayscale2.colorMode-light a, +body.colorScheme-grayscale2.colorMode-light li, +body.colorScheme-grayscale2.colorMode-light input, +body.colorScheme-grayscale2.colorMode-light select, +body.colorScheme-grayscale2.colorMode-light textarea, +body.colorScheme-grayscale2.colorMode-light button, +body.colorScheme-grayscale2.colorMode-light .selectableContent { + color: black !important; +} + +body.colorScheme-grayscale2.colorMode-dark, +body.colorScheme-grayscale2.colorMode-dark h1, +body.colorScheme-grayscale2.colorMode-dark h2, +body.colorScheme-grayscale2.colorMode-dark h3, +body.colorScheme-grayscale2.colorMode-dark h4, +body.colorScheme-grayscale2.colorMode-dark h5, +body.colorScheme-grayscale2.colorMode-dark h6, +body.colorScheme-grayscale2.colorMode-dark p, +body.colorScheme-grayscale2.colorMode-dark span, +body.colorScheme-grayscale2.colorMode-dark td, +body.colorScheme-grayscale2.colorMode-dark th, +body.colorScheme-grayscale2.colorMode-dark label, +body.colorScheme-grayscale2.colorMode-dark a, +body.colorScheme-grayscale2.colorMode-dark li, +body.colorScheme-grayscale2.colorMode-dark input, +body.colorScheme-grayscale2.colorMode-dark select, +body.colorScheme-grayscale2.colorMode-dark textarea, +body.colorScheme-grayscale2.colorMode-dark button, +body.colorScheme-grayscale2.colorMode-dark .selectableContent { + color: white !important; +} + +body.colorScheme-themeDefault.colorMode-light, +body.colorScheme-themeDefault.colorMode-light h2, +body.colorScheme-themeDefault.colorMode-light h3, +body.colorScheme-themeDefault.colorMode-light h4, +body.colorScheme-themeDefault.colorMode-light h5, +body.colorScheme-themeDefault.colorMode-light h6, +body.colorScheme-themeDefault.colorMode-light p, +body.colorScheme-themeDefault.colorMode-light td, +body.colorScheme-themeDefault.colorMode-light th, +body.colorScheme-themeDefault.colorMode-light label, +body.colorScheme-themeDefault.colorMode-light li, +body.colorScheme-themeDefault.colorMode-light select, +body.colorScheme-themeDefault.colorMode-light textarea, +body.colorScheme-themeDefault.colorMode-light .selectableContent { + color: #006E9F !important; +} + +body.colorScheme-themeDefault.colorMode-dark, +body.colorScheme-themeDefault.colorMode-dark h1, +body.colorScheme-themeDefault.colorMode-dark h2, +body.colorScheme-themeDefault.colorMode-dark h3, +body.colorScheme-themeDefault.colorMode-dark h4, +body.colorScheme-themeDefault.colorMode-dark h5, +body.colorScheme-themeDefault.colorMode-dark h6, +body.colorScheme-themeDefault.colorMode-dark p, +body.colorScheme-themeDefault.colorMode-dark td, +body.colorScheme-themeDefault.colorMode-dark th, +body.colorScheme-themeDefault.colorMode-dark a, +body.colorScheme-themeDefault.colorMode-dark label, +body.colorScheme-themeDefault.colorMode-dark input, +body.colorScheme-themeDefault.colorMode-dark select, +body.colorScheme-themeDefault.colorMode-dark textarea, +body.colorScheme-themeDefault.colorMode-dark .selectableContent { + color: white !important; +} \ No newline at end of file From 82a08d7e426338292c20f3d24fd3566a615ff91f Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 17 Feb 2026 13:19:01 +0100 Subject: [PATCH 08/24] #5519 Add initial structure of repository, template, controller and service --- .../Controllers/CustomTemplatesController.php | 68 ++++++++++ .../CustomTemplatesRepository.php | 66 ++++++++++ .../Services/CustomTemplatesService.php | 49 ++++++++ .../Templates/customTemplates.blade.php | 117 ++++++++++++++++++ app/Domain/Wiki/Templates/templates.tpl.php | 24 +++- 5 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 app/Domain/Tickets/Controllers/CustomTemplatesController.php create mode 100644 app/Domain/Tickets/Repositories/CustomTemplatesRepository.php create mode 100644 app/Domain/Tickets/Services/CustomTemplatesService.php create mode 100644 app/Domain/Tickets/Templates/customTemplates.blade.php diff --git a/app/Domain/Tickets/Controllers/CustomTemplatesController.php b/app/Domain/Tickets/Controllers/CustomTemplatesController.php new file mode 100644 index 0000000000..39f4ce4566 --- /dev/null +++ b/app/Domain/Tickets/Controllers/CustomTemplatesController.php @@ -0,0 +1,68 @@ +templateService = $templateService; + } + + public function showAll($params) + { + $userId = $_SESSION['userdata']['id']; + + $templates = $this->templateService->getAllTemplates($userId); + + $this->tpl->assign('templates', $templates); + + return $this->tpl->display('tickets.templates'); + } + + public function create($params) + { + return $this->tpl->display('tickets.createTemplate'); + } + + public function save($params) + { + $userId = $_SESSION['userdata']['id']; + + $title = $_POST['title'] ?? ''; + $content = $_POST['content'] ?? ''; + + $result = $this->templateService->createTemplate($title, $content, $userId); + + if ($result['success']) { + $this->tpl->setNotification($result['message'], 'success'); + return $this->tpl->redirect(BASE_URL . '/tickets/customTemplates/showAll'); + } else { + $this->tpl->setNotification($result['message'], 'error'); + $this->tpl->assign('title', $title); + $this->tpl->assign('content', $content); + return $this->tpl->display('tickets.createTemplate'); + } + } + + public function delete($params) + { + $userId = $_SESSION['userdata']['id']; + $templateId = $params['id'] ?? null; + + if (!$templateId) { + $this->tpl->setNotification('Invalid template ID', 'error'); + return $this->tpl->redirect(BASE_URL . '/tickets/customTemplates/showAll'); + } + + $result = $this->templateService->deleteTemplate($templateId, $userId); + + $this->tpl->setNotification($result['message'], $result['success'] ? 'success' : 'error'); + return $this->tpl->redirect(BASE_URL . '/tickets/customTemplates/showAll'); + } +} diff --git a/app/Domain/Tickets/Repositories/CustomTemplatesRepository.php b/app/Domain/Tickets/Repositories/CustomTemplatesRepository.php new file mode 100644 index 0000000000..efc360216d --- /dev/null +++ b/app/Domain/Tickets/Repositories/CustomTemplatesRepository.php @@ -0,0 +1,66 @@ +db = $db; + } + + public function getAll($userId) + { + $sql = "SELECT * FROM zp_ticket_custom_templates + WHERE userId = :userId + ORDER BY title"; + + $stmn = $this->db->database->prepare($sql); + $stmn->bindValue(':userId', $userId, \PDO::PARAM_INT); + $stmn->execute(); + + return $stmn->fetchAll(\PDO::FETCH_ASSOC); + } + + public function create($title, $content, $userId) + { + $sql = "INSERT INTO zp_ticket_custom_templates (title, content, userId) + VALUES (:title, :content, :userId)"; + + $stmn = $this->db->database->prepare($sql); + $stmn->bindValue(':title', $title, \PDO::PARAM_STR); + $stmn->bindValue(':content', $content, \PDO::PARAM_STR); + $stmn->bindValue(':userId', $userId, \PDO::PARAM_INT); + + return $stmn->execute(); + } + + public function delete($id, $userId) + { + $sql = "DELETE FROM zp_ticket_custom_templates + WHERE id = :id AND userId = :userId"; + + $stmn = $this->db->database->prepare($sql); + $stmn->bindValue(':id', $id, \PDO::PARAM_INT); + $stmn->bindValue(':userId', $userId, \PDO::PARAM_INT); + + return $stmn->execute(); + } + + public function getOne($id, $userId) + { + $sql = "SELECT * FROM zp_ticket_custom_templates + WHERE id = :id AND userId = :userId"; + + $stmn = $this->db->database->prepare($sql); + $stmn->bindValue(':id', $id, \PDO::PARAM_INT); + $stmn->bindValue(':userId', $userId, \PDO::PARAM_INT); + $stmn->execute(); + + return $stmn->fetch(\PDO::FETCH_ASSOC); + } +} diff --git a/app/Domain/Tickets/Services/CustomTemplatesService.php b/app/Domain/Tickets/Services/CustomTemplatesService.php new file mode 100644 index 0000000000..6aa64587cf --- /dev/null +++ b/app/Domain/Tickets/Services/CustomTemplatesService.php @@ -0,0 +1,49 @@ +customTemplatesRepo = $customTemplatesRepo; + } + + public function getAllTemplates($userId) + { + return $this->customTemplatesRepo->getAll($userId); + } + + public function createTemplate($title, $content, $userId) + { + if (empty($title) || empty($content)) { + return ['success' => false, 'message' => 'Title and content required']; + } + + $result = $this->customTemplatesRepo->create($title, $content, $userId); + + return [ + 'success' => $result, + 'message' => $result ? 'Template saved' : 'Failed to save' + ]; + } + + public function deleteTemplate($id, $userId) + { + $result = $this->customTemplatesRepo->delete($id, $userId); + + return [ + 'success' => $result, + 'message' => $result ? 'Template deleted' : 'Not found or not authorized' + ]; + } + + public function getTemplate($id, $userId) + { + return $this->customTemplatesRepo->getOne($id, $userId); + } +} diff --git a/app/Domain/Tickets/Templates/customTemplates.blade.php b/app/Domain/Tickets/Templates/customTemplates.blade.php new file mode 100644 index 0000000000..560453fe8c --- /dev/null +++ b/app/Domain/Tickets/Templates/customTemplates.blade.php @@ -0,0 +1,117 @@ +@extends('layouts.app') + +@section('content') + + +
+
+ + @if(empty($templates)) +
+ +

No templates yet

+

Save your frequently used ticket descriptions as templates for quick reuse.

+ + Create Your First Template + +
+ @else +
+ @foreach($templates as $template) +
+
+

+ {{ $template['title'] }} +

+
+ {{ date('M d, Y', strtotime($template['created'])) }} +
+
+ {!! substr(strip_tags($template['content']), 0, 150) !!}... +
+
+ + + + +
+
+
+ @endforeach +
+ @endif + +
+
+ + + + + +@endsection \ No newline at end of file diff --git a/app/Domain/Wiki/Templates/templates.tpl.php b/app/Domain/Wiki/Templates/templates.tpl.php index d91f109ee5..5a6d678872 100644 --- a/app/Domain/Wiki/Templates/templates.tpl.php +++ b/app/Domain/Wiki/Templates/templates.tpl.php @@ -1,6 +1,8 @@ $val) { $$var = $val; // necessary for blade refactor @@ -488,7 +490,27 @@ $labelGray->description = $tpl->__('templates.titles.gray_status_description'); $labelGray->content = 'Gray'; $templates[] = $labelGray; +try { + $customTemplatesRepo = app()->make(CustomTemplatesRepository::class); + $userId = $_SESSION['userdata']['id'] ?? null; + + if ($userId) { + $customTemplatesData = $customTemplatesRepo->getAll($userId); + + foreach ($customTemplatesData as $customTemplate) { + $customTpl = app()->make(Template::class); + $customTpl->title = $customTemplate['title']; + $customTpl->category = 'My Custom Templates'; + $customTpl->description = 'Custom template'; + $customTpl->content = $customTemplate['content']; + + $templates[] = $customTpl; + } + } +} catch (\Exception $e) { + error_log('Failed to load custom templates: ' . $e->getMessage()); +} $templates = $tpl->dispatch_filter('documentTemplates', $templates); -echo json_encode($templates); +echo json_encode($templates); \ No newline at end of file From 2bf788fd24534f219b39db195cc1bb5b98fef5ee Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 17 Feb 2026 15:02:22 +0100 Subject: [PATCH 09/24] #5519 Fix controller, enable saving templates to database --- .../Tickets/Controllers/CustomTemplates.php | 110 ++++++++++++++++++ .../Controllers/CustomTemplatesController.php | 68 ----------- .../Services/CustomTemplatesService.php | 2 +- .../submodules/ticketDetails.sub.php | 57 +++++++++ app/Domain/Wiki/Templates/templates.tpl.php | 7 ++ 5 files changed, 175 insertions(+), 69 deletions(-) create mode 100644 app/Domain/Tickets/Controllers/CustomTemplates.php delete mode 100644 app/Domain/Tickets/Controllers/CustomTemplatesController.php diff --git a/app/Domain/Tickets/Controllers/CustomTemplates.php b/app/Domain/Tickets/Controllers/CustomTemplates.php new file mode 100644 index 0000000000..2d4efbe067 --- /dev/null +++ b/app/Domain/Tickets/Controllers/CustomTemplates.php @@ -0,0 +1,110 @@ +templateService = $templateService; + } + + private function getUserId(): ?int + { + return session('userdata.id'); + } + + private function jsonAuth(): void + { + $userId = $this->getUserId(); + if (!$userId) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Not authenticated']); + exit; + } + } + + public function get($params) + { + $this->jsonAuth(); + $userId = $this->getUserId(); + $templates = $this->templateService->getAllTemplates($userId); + + $formatted = array_map(function($tpl) { + return [ + 'title' => $tpl['title'], + 'content' => $tpl['content'], + ]; + }, $templates); + + header('Content-Type: application/json'); + echo json_encode($formatted); + exit; + } + + public function save($params) + { + $this->jsonAuth(); + $userId = $this->getUserId(); + + $title = $_POST['title'] ?? ''; + $content = $_POST['content'] ?? ''; + + $result = $this->templateService->createTemplate($title, $content, $userId); + + header('Content-Type: application/json'); + echo json_encode($result); + exit; + } + + public function showAll($params) + { + $userId = $this->getUserId(); + if (!$userId) { + return Frontcontroller::redirect(BASE_URL . '/login'); + } + + $templates = $this->templateService->getAllTemplates($userId); + $this->tpl->assign('templates', $templates); + return $this->tpl->display('tickets.customTemplates'); + } + + public function create($params) + { + $userId = $this->getUserId(); + if (!$userId) { + return Frontcontroller::redirect(BASE_URL . '/login'); + } + + return $this->tpl->display('tickets.createTemplate'); + } + + public function delete($params) + { + $userId = $this->getUserId(); + if (!$userId) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Not authenticated']); + exit; + } + + $templateId = $params['id'] ?? null; + if (!$templateId) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid template ID']); + exit; + } + + $result = $this->templateService->deleteTemplate($templateId, $userId); + + header('Content-Type: application/json'); + echo json_encode($result); + exit; + } +} \ No newline at end of file diff --git a/app/Domain/Tickets/Controllers/CustomTemplatesController.php b/app/Domain/Tickets/Controllers/CustomTemplatesController.php deleted file mode 100644 index 39f4ce4566..0000000000 --- a/app/Domain/Tickets/Controllers/CustomTemplatesController.php +++ /dev/null @@ -1,68 +0,0 @@ -templateService = $templateService; - } - - public function showAll($params) - { - $userId = $_SESSION['userdata']['id']; - - $templates = $this->templateService->getAllTemplates($userId); - - $this->tpl->assign('templates', $templates); - - return $this->tpl->display('tickets.templates'); - } - - public function create($params) - { - return $this->tpl->display('tickets.createTemplate'); - } - - public function save($params) - { - $userId = $_SESSION['userdata']['id']; - - $title = $_POST['title'] ?? ''; - $content = $_POST['content'] ?? ''; - - $result = $this->templateService->createTemplate($title, $content, $userId); - - if ($result['success']) { - $this->tpl->setNotification($result['message'], 'success'); - return $this->tpl->redirect(BASE_URL . '/tickets/customTemplates/showAll'); - } else { - $this->tpl->setNotification($result['message'], 'error'); - $this->tpl->assign('title', $title); - $this->tpl->assign('content', $content); - return $this->tpl->display('tickets.createTemplate'); - } - } - - public function delete($params) - { - $userId = $_SESSION['userdata']['id']; - $templateId = $params['id'] ?? null; - - if (!$templateId) { - $this->tpl->setNotification('Invalid template ID', 'error'); - return $this->tpl->redirect(BASE_URL . '/tickets/customTemplates/showAll'); - } - - $result = $this->templateService->deleteTemplate($templateId, $userId); - - $this->tpl->setNotification($result['message'], $result['success'] ? 'success' : 'error'); - return $this->tpl->redirect(BASE_URL . '/tickets/customTemplates/showAll'); - } -} diff --git a/app/Domain/Tickets/Services/CustomTemplatesService.php b/app/Domain/Tickets/Services/CustomTemplatesService.php index 6aa64587cf..8f42cd249b 100644 --- a/app/Domain/Tickets/Services/CustomTemplatesService.php +++ b/app/Domain/Tickets/Services/CustomTemplatesService.php @@ -46,4 +46,4 @@ public function getTemplate($id, $userId) { return $this->customTemplatesRepo->getOne($id, $userId); } -} +} \ No newline at end of file diff --git a/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php b/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php index b9a9acfb01..46b5d085a9 100644 --- a/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php +++ b/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php @@ -178,6 +178,63 @@ class="autosave-field"
+
+ +
+ + diff --git a/app/Domain/Wiki/Templates/templates.tpl.php b/app/Domain/Wiki/Templates/templates.tpl.php index 5a6d678872..1e605d89c2 100644 --- a/app/Domain/Wiki/Templates/templates.tpl.php +++ b/app/Domain/Wiki/Templates/templates.tpl.php @@ -490,6 +490,13 @@ $labelGray->description = $tpl->__('templates.titles.gray_status_description'); $labelGray->content = 'Gray'; $templates[] = $labelGray; +$saveCurrentTemplate = app()->make(Template::class); +$saveCurrentTemplate->title = '💾 Save Current Content as Template'; +$saveCurrentTemplate->category = 'My Custom Templates'; +$saveCurrentTemplate->description = 'Save what you just wrote as a reusable template'; +$saveCurrentTemplate->content = '

Click to save current editor content as a template

'; +$templates[] = $saveCurrentTemplate; + try { $customTemplatesRepo = app()->make(CustomTemplatesRepository::class); $userId = $_SESSION['userdata']['id'] ?? null; From e275448a87b639c36a0c50c33dda920662c948e0 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 17 Feb 2026 15:11:55 +0100 Subject: [PATCH 10/24] #5519 Add logic for selecting templates in ticket description --- app/Domain/Wiki/Templates/templates.tpl.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Domain/Wiki/Templates/templates.tpl.php b/app/Domain/Wiki/Templates/templates.tpl.php index 1e605d89c2..95fcfc8bf9 100644 --- a/app/Domain/Wiki/Templates/templates.tpl.php +++ b/app/Domain/Wiki/Templates/templates.tpl.php @@ -498,8 +498,9 @@ $templates[] = $saveCurrentTemplate; try { - $customTemplatesRepo = app()->make(CustomTemplatesRepository::class); - $userId = $_SESSION['userdata']['id'] ?? null; + $customTemplatesRepo = app()->make(\Leantime\Domain\Tickets\Repositories\CustomTemplatesRepository::class); + + $userId = session('userdata.id'); if ($userId) { $customTemplatesData = $customTemplatesRepo->getAll($userId); @@ -508,9 +509,8 @@ $customTpl = app()->make(Template::class); $customTpl->title = $customTemplate['title']; $customTpl->category = 'My Custom Templates'; - $customTpl->description = 'Custom template'; + $customTpl->description = $customTemplate['title']; // or a real description field if you have one $customTpl->content = $customTemplate['content']; - $templates[] = $customTpl; } } From 175117052e46e0ba3b16b607df72cbb9a6d43204 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 17 Feb 2026 15:23:53 +0100 Subject: [PATCH 11/24] [#5519] Remove comments and unused component --- .../Tickets/Controllers/CustomTemplates.php | 1 + .../Templates/customTemplates.blade.php | 117 ------------------ .../submodules/ticketDetails.sub.php | 9 +- app/Domain/Wiki/Templates/templates.tpl.php | 10 +- 4 files changed, 4 insertions(+), 133 deletions(-) delete mode 100644 app/Domain/Tickets/Templates/customTemplates.blade.php diff --git a/app/Domain/Tickets/Controllers/CustomTemplates.php b/app/Domain/Tickets/Controllers/CustomTemplates.php index 2d4efbe067..6876bc9704 100644 --- a/app/Domain/Tickets/Controllers/CustomTemplates.php +++ b/app/Domain/Tickets/Controllers/CustomTemplates.php @@ -6,6 +6,7 @@ use Leantime\Domain\Tickets\Services\CustomTemplatesService; use Leantime\Core\Controller\Frontcontroller; + class CustomTemplates extends Controller { private CustomTemplatesService $templateService; diff --git a/app/Domain/Tickets/Templates/customTemplates.blade.php b/app/Domain/Tickets/Templates/customTemplates.blade.php deleted file mode 100644 index 560453fe8c..0000000000 --- a/app/Domain/Tickets/Templates/customTemplates.blade.php +++ /dev/null @@ -1,117 +0,0 @@ -@extends('layouts.app') - -@section('content') - - -
-
- - @if(empty($templates)) -
- -

No templates yet

-

Save your frequently used ticket descriptions as templates for quick reuse.

- - Create Your First Template - -
- @else -
- @foreach($templates as $template) -
-
-

- {{ $template['title'] }} -

-
- {{ date('M d, Y', strtotime($template['created'])) }} -
-
- {!! substr(strip_tags($template['content']), 0, 150) !!}... -
-
- - - - -
-
-
- @endforeach -
- @endif - -
-
- - - - - -@endsection \ No newline at end of file diff --git a/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php b/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php index 46b5d085a9..a64054023a 100644 --- a/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php +++ b/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php @@ -190,14 +190,12 @@ class="complexEditor">description !== null ? htmlentities($t e.preventDefault(); e.stopPropagation(); - // Use simple prompt instead of modal var templateName = prompt('Enter a name for this template:'); if (!templateName || templateName.trim() === '') { return; } - // Get content from TinyMCE var content = ''; if (typeof tinymce !== 'undefined' && tinymce.get('ticketDescription')) { content = tinymce.get('ticketDescription').getContent(); @@ -210,10 +208,6 @@ class="complexEditor">description !== null ? htmlentities($t return; } - console.log('Saving template:', templateName); - console.log('Content length:', content.length); - - // Save via AJAX - FIXED URL jQuery.ajax({ url: '/tickets/customTemplates/save', method: 'POST', @@ -222,8 +216,7 @@ class="complexEditor">description !== null ? htmlentities($t content: content }, success: function(response) { - console.log('Success response:', response); - alert('Template saved successfully! Refresh to see it in TinyMCE templates.'); + alert('Template saved successfully!'); }, error: function(xhr, status, error) { console.error('Error status:', status); diff --git a/app/Domain/Wiki/Templates/templates.tpl.php b/app/Domain/Wiki/Templates/templates.tpl.php index 95fcfc8bf9..5cea4730e3 100644 --- a/app/Domain/Wiki/Templates/templates.tpl.php +++ b/app/Domain/Wiki/Templates/templates.tpl.php @@ -490,12 +490,6 @@ $labelGray->description = $tpl->__('templates.titles.gray_status_description'); $labelGray->content = 'Gray'; $templates[] = $labelGray; -$saveCurrentTemplate = app()->make(Template::class); -$saveCurrentTemplate->title = '💾 Save Current Content as Template'; -$saveCurrentTemplate->category = 'My Custom Templates'; -$saveCurrentTemplate->description = 'Save what you just wrote as a reusable template'; -$saveCurrentTemplate->content = '

Click to save current editor content as a template

'; -$templates[] = $saveCurrentTemplate; try { $customTemplatesRepo = app()->make(\Leantime\Domain\Tickets\Repositories\CustomTemplatesRepository::class); @@ -509,7 +503,7 @@ $customTpl = app()->make(Template::class); $customTpl->title = $customTemplate['title']; $customTpl->category = 'My Custom Templates'; - $customTpl->description = $customTemplate['title']; // or a real description field if you have one + $customTpl->description = $customTemplate['title']; $customTpl->content = $customTemplate['content']; $templates[] = $customTpl; } @@ -520,4 +514,4 @@ $templates = $tpl->dispatch_filter('documentTemplates', $templates); -echo json_encode($templates); \ No newline at end of file +echo json_encode($templates); From a7fe48a3b2cd0249743eef14afee5aec533a1d55 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 18 Feb 2026 17:27:29 +0100 Subject: [PATCH 12/24] #5519 Replace built in dialog pop up with custom modal --- .../Tickets/Controllers/CustomTemplates.php | 4 +- .../submodules/ticketDetails.sub.php | 80 +++++++++++++++---- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/app/Domain/Tickets/Controllers/CustomTemplates.php b/app/Domain/Tickets/Controllers/CustomTemplates.php index 6876bc9704..5b57a087f0 100644 --- a/app/Domain/Tickets/Controllers/CustomTemplates.php +++ b/app/Domain/Tickets/Controllers/CustomTemplates.php @@ -37,7 +37,7 @@ public function get($params) $userId = $this->getUserId(); $templates = $this->templateService->getAllTemplates($userId); - $formatted = array_map(function($tpl) { + $formatted = array_map(function ($tpl) { return [ 'title' => $tpl['title'], 'content' => $tpl['content'], @@ -108,4 +108,4 @@ public function delete($params) echo json_encode($result); exit; } -} \ No newline at end of file +} diff --git a/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php b/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php index a64054023a..ec9f37660a 100644 --- a/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php +++ b/app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php @@ -183,46 +183,92 @@ class="complexEditor">description !== null ? htmlentities($t Save as Template + \ No newline at end of file + diff --git a/public/assets/js/app/core/datePickers.js b/public/assets/js/app/core/datePickers.js index 6f25778929..876d214614 100644 --- a/public/assets/js/app/core/datePickers.js +++ b/public/assets/js/app/core/datePickers.js @@ -119,7 +119,7 @@ leantime.dateController = (function () { }, ranges: { 'Today': [moment().startOf('day'), moment().endOf('day')], - 'This Week': [moment().startOf('week'), moment().endOf('week')], + 'This Week': [moment().startOf('isoWeek'), moment().endOf('isoWeek')], 'This Month': [moment().startOf('month'), moment().endOf('month')], 'Last 7 Days': [moment().subtract(6, 'days'), moment()] } diff --git a/public/assets/js/app/timesheets/filterPreferences.js b/public/assets/js/app/timesheets/filterPreferences.js index 45e1d1a370..c710623d22 100644 --- a/public/assets/js/app/timesheets/filterPreferences.js +++ b/public/assets/js/app/timesheets/filterPreferences.js @@ -104,7 +104,7 @@ }); dateInput.on('apply.daterangepicker', function (ev, picker) { - if (picker.chosenLabel && picker.chosenLabel !== 'Custom Range') { + if (picker.chosenLabel && picker.chosenLabel !== 'Custom Range' && picker.chosenLabel !== 'Custom') { selectedRangeName = picker.chosenLabel; picker._storedChosenLabel = picker.chosenLabel; } @@ -144,6 +144,7 @@ localStorage.setItem('activeProfileDateRange', data.preference.filters.dateRange || 'Custom'); localStorage.setItem('activeProfileLastApplied', new Date().toISOString()); updateActiveProfileDisplay(); + window._timesheetSubmitFromProfileLoad = true; jQuery('#form').submit(); return true; } else { @@ -162,11 +163,18 @@ function updateActiveProfileDisplay() { const button = jQuery('#filterPreferencesBtn'); + const exportProfileInput = document.getElementById('timesheetExportProfile'); if (activeProfileName) { button.html(` Profile: ${activeProfileName}`); + if (exportProfileInput) { + exportProfileInput.value = activeProfileName; + } } else { button.html(` None selected`); button.css('background-color', ''); + if (exportProfileInput) { + exportProfileInput.value = ''; + } } } @@ -236,7 +244,11 @@ const formElement = document.getElementById('form'); if (formElement) { formElement.addEventListener('submit', function(e) { - if (activeProfileName && !isEditMode && !isApplyingFilters) { + if (window._timesheetSubmitFromProfileLoad) { + window._timesheetSubmitFromProfileLoad = false; + } else if (window._timesheetSubmitFromExportToSlack) { + window._timesheetSubmitFromExportToSlack = false; + } else if (activeProfileName && !isEditMode && !isApplyingFilters) { clearActiveProfile(); } @@ -462,11 +474,14 @@ if (dateInput.length && dateInput.data('daterangepicker')) { const picker = dateInput.data('daterangepicker'); - if (picker._storedChosenLabel && picker._storedChosenLabel !== 'Custom Range') { + const isPreset = function (label) { + return label && label !== 'Custom Range' && label !== 'Custom'; + }; + if (isPreset(picker._storedChosenLabel)) { dateRange = picker._storedChosenLabel; - } else if (picker.chosenLabel && picker.chosenLabel !== 'Custom Range') { + } else if (isPreset(picker.chosenLabel)) { dateRange = picker.chosenLabel; - } else if (selectedRangeName && selectedRangeName !== 'Custom Range') { + } else if (isPreset(selectedRangeName)) { dateRange = selectedRangeName; } else { dateRange = detectRangeFromDates(dateFrom, dateTo); @@ -524,8 +539,10 @@ function detectRangeFromDates(dateFrom, dateTo) { if (!dateFrom || !dateTo) return 'Custom'; - const from = moment(dateFrom, 'MM/DD/YYYY'); - const to = moment(dateTo, 'MM/DD/YYYY'); + const from = moment(dateFrom, ['YYYY-MM-DD', 'MM/DD/YYYY'], true); + const to = moment(dateTo, ['YYYY-MM-DD', 'MM/DD/YYYY'], true); + if (!from.isValid() || !to.isValid()) return 'Custom'; + const today = moment().startOf('day'); if (from.isSame(today, 'day') && to.isSame(today, 'day')) { @@ -544,6 +561,10 @@ return 'This Month'; } + const last7Start = moment().subtract(6, 'days').startOf('day'); + if (from.isSame(last7Start, 'day') && to.isSame(today, 'day')) { + return 'Last 7 Days'; + } return 'Custom'; } @@ -1048,6 +1069,40 @@ saveAutoExportSetting(profileName, isEnabled); }); + jQuery(document).on('click', '#exportToSlackBtn', function (e) { + e.preventDefault(); + e.stopPropagation(); + const btn = this; + const form = document.getElementById('form'); + const hiddenInput = document.getElementById('timesheetExportProfile'); + const slackUrl = btn.getAttribute('formaction') || (leantime.appUrl + '/timesheets/slackMonthlyReportController/sendCsvFromUsersProfilesWhichHaveTickboxTrue'); + const profileName = localStorage.getItem('activeProfileName') || ''; + + function doSubmit() { + if (hiddenInput) { + hiddenInput.value = profileName; + } + window._timesheetSubmitFromExportToSlack = true; + form.action = slackUrl; + form.submit(); + } + + if (profileName) { + loadAllPreferences().then(function () { + const pref = currentPreferences[profileName]; + if (pref && pref.filters) { + applyFilters(pref.filters); + setTimeout(doSubmit, 450); + } else { + doSubmit(); + } + }).catch(function () { + doSubmit(); + }); + } else { + doSubmit(); + } + }); } async function saveAutoExportSetting(profileName, enabled) { @@ -1084,4 +1139,4 @@ getCurrent: getCurrentFilters, apply: applyFilters } -})() \ No newline at end of file +})() From 6a724679b1a4e07020719e4002920262134773bc Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:18:56 +0100 Subject: [PATCH 16/24] #6015 - Removed 2 second pause beetween sending of slack exports --- app/Domain/Timesheets/Services/SlackMonthlyReportService.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Domain/Timesheets/Services/SlackMonthlyReportService.php b/app/Domain/Timesheets/Services/SlackMonthlyReportService.php index 2492685271..d6a646d28e 100644 --- a/app/Domain/Timesheets/Services/SlackMonthlyReportService.php +++ b/app/Domain/Timesheets/Services/SlackMonthlyReportService.php @@ -339,8 +339,6 @@ public function sendMonthlyReportToSlack(array $profilesWithEnabledAutoExport, s } else { $failCount++; } - - sleep(2); } if ($successCount > 0 && $failCount === 0) { From bcef18326fdf537ab1f34191f82063464da29eb0 Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:14:39 +0100 Subject: [PATCH 17/24] #6015 Add upload rate exceeded error notification --- .../Services/SlackMonthlyReportService.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/Domain/Timesheets/Services/SlackMonthlyReportService.php b/app/Domain/Timesheets/Services/SlackMonthlyReportService.php index d6a646d28e..b8cea6888c 100644 --- a/app/Domain/Timesheets/Services/SlackMonthlyReportService.php +++ b/app/Domain/Timesheets/Services/SlackMonthlyReportService.php @@ -380,6 +380,7 @@ private function sendCsvToSlack(string $csvContent, string $profileName, string $filename = "timesheet_{$profileName}_" . date('Y-m-d') . ".csv"; $getUploadUrlResponse = $this->httpClient->post('https://slack.com/api/files.getUploadURLExternal', [ + 'http_errors' => false, 'headers' => [ 'Authorization' => "Bearer {$slackBotToken}", 'Content-Type' => 'application/x-www-form-urlencoded', @@ -390,6 +391,15 @@ private function sendCsvToSlack(string $csvContent, string $profileName, string ] ]); + if ($getUploadUrlResponse->getStatusCode() === 429) { + $this->tpl->setNotification( + 'Slack upload rate exceeded. Please try again later.', + 'error', + 'save_timesheet' + ); + return false; + } + $uploadUrlData = json_decode($getUploadUrlResponse->getBody()->getContents(), true); if (!isset($uploadUrlData['ok']) || !$uploadUrlData['ok']) { @@ -411,6 +421,7 @@ private function sendCsvToSlack(string $csvContent, string $profileName, string } $completeResponse = $this->httpClient->post('https://slack.com/api/files.completeUploadExternal', [ + 'http_errors' => false, 'headers' => [ 'Authorization' => "Bearer {$slackBotToken}", 'Content-Type' => 'application/json', @@ -426,13 +437,23 @@ private function sendCsvToSlack(string $csvContent, string $profileName, string 'initial_comment' => "Monthly Timesheet Report: {$profileName}" ] ]); + + if ($completeResponse->getStatusCode() === 429) { + $this->tpl->setNotification( + 'Slack upload rate exceeded. Please try again later.', + 'error', + 'save_timesheet' + ); + return false; + } $completeData = json_decode($completeResponse->getBody()->getContents(), true); if (isset($completeData['ok']) && $completeData['ok']) { return true; + } else { + return false; } - return false; } catch (\GuzzleHttp\Exception\GuzzleException $e) { report($e); From f3be9855b4302e8c232de1a8b7cf4ba779cae87d Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Thu, 26 Feb 2026 09:55:14 +0100 Subject: [PATCH 18/24] #6078 Add logic for milestone filters in ShowAll controller, Timesheets service, Timesheets repos and showAll view --- app/Domain/Timesheets/Controllers/ShowAll.php | 23 ++++++++++++++++++- .../Timesheets/Repositories/Timesheets.php | 8 ++++++- app/Domain/Timesheets/Services/Timesheets.php | 5 ++-- .../Timesheets/Templates/showAll.tpl.php | 12 ++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/Domain/Timesheets/Controllers/ShowAll.php b/app/Domain/Timesheets/Controllers/ShowAll.php index 852b441cdf..f7cfbf6ce7 100644 --- a/app/Domain/Timesheets/Controllers/ShowAll.php +++ b/app/Domain/Timesheets/Controllers/ShowAll.php @@ -164,6 +164,26 @@ public function run(): Response } } } +$milestoneFilter = -1; +if (! empty($_POST['milestoneId'])) { + $milestoneFilter = (int) strip_tags($_POST['milestoneId']); +} + +$milestonesProject = is_array($projectFilter) ? -1 : (int) $projectFilter; +if ($milestonesProject > 0) { + $allMilestones = $this->ticketService->getAllMilestones([ + 'currentProject' => $milestonesProject + ]); +} else { + $allMilestones = $this->ticketService->getAll([ + 'type' => 'milestone', + 'currentProject' => '', + 'excludeType' => '', + 'currentUser' => '' + ]); +} +$this->tpl->assign('milestoneFilter', $milestoneFilter); +$this->tpl->assign('allMilestones', $allMilestones); $user = app()->make(UserRepository::class); $employees = $user->getAll(); @@ -208,7 +228,8 @@ public function run(): Response $invCompCheck, $ticketParameter, $paidCheck, - $clientId + $clientId, + $milestoneFilter )); // Pass user's hours format preference to template for CSV export diff --git a/app/Domain/Timesheets/Repositories/Timesheets.php b/app/Domain/Timesheets/Repositories/Timesheets.php index 77719bc168..708fc00180 100644 --- a/app/Domain/Timesheets/Repositories/Timesheets.php +++ b/app/Domain/Timesheets/Repositories/Timesheets.php @@ -37,7 +37,7 @@ public function __construct(DbCore $db) * * @return array|false An array of timesheets or false if there was an error */ - public function getAll(int|array|null $id, ?string $kind, ?CarbonInterface $dateFrom, ?CarbonInterface $dateTo, ?int $userId, ?string $invEmpl, ?string $invComp, ?string $paid, ?int $clientId, ?int $ticketFilter): array|false + public function getAll(int|array|null $id, ?string $kind, ?CarbonInterface $dateFrom, ?CarbonInterface $dateTo, ?int $userId, ?string $invEmpl, ?string $invComp, ?string $paid, ?int $clientId, ?int $ticketFilter, int $milestoneId = -1): array|false { $sanitizedProjectIds = []; $singleProjectId = null; @@ -126,6 +126,9 @@ public function getAll(int|array|null $id, ?string $kind, ?CarbonInterface $date if ($paid == '1') { $query .= ' AND (zp_timesheets.paid = 1)'; } + if ($milestoneId > 0) { + $query .= ' AND (zp_tickets.milestoneid = :milestoneId)'; +} $query .= ' GROUP BY zp_timesheets.id, @@ -166,6 +169,9 @@ public function getAll(int|array|null $id, ?string $kind, ?CarbonInterface $date if ($userId != 'all' && $userId != null) { $call->bindValue(':userId', $userId); } + if ($milestoneId > 0) { + $call->bindValue(':milestoneId', $milestoneId, PDO::PARAM_INT); + } return $call->fetchAll(); } diff --git a/app/Domain/Timesheets/Services/Timesheets.php b/app/Domain/Timesheets/Services/Timesheets.php index 36fd9196ce..7801650a74 100644 --- a/app/Domain/Timesheets/Services/Timesheets.php +++ b/app/Domain/Timesheets/Services/Timesheets.php @@ -343,7 +343,7 @@ public function getLoggableHourTypes(): array /** * @api */ - public function getAll(CarbonInterface $dateFrom, CarbonInterface $dateTo, int|array $projectId = -1, string $kind = 'all', ?int $userId = null, string $invEmpl = '-1', string $invComp = '-1', string $ticketFilter = '-1', string $paid = '-1', string $clientId = '-1'): array|false + public function getAll(CarbonInterface $dateFrom, CarbonInterface $dateTo, int|array $projectId = -1, string $kind = 'all', ?int $userId = null, string $invEmpl = '-1', string $invComp = '-1', string $ticketFilter = '-1', string $paid = '-1', string $clientId = '-1', int $milestoneId = -1): array|false { return $this->timesheetsRepo->getAll( id: $projectId, @@ -355,7 +355,8 @@ public function getAll(CarbonInterface $dateFrom, CarbonInterface $dateTo, int|a invComp: $invComp, paid: $paid, clientId: $clientId, - ticketFilter: $ticketFilter + ticketFilter: $ticketFilter, + milestoneId: $milestoneId ); } diff --git a/app/Domain/Timesheets/Templates/showAll.tpl.php b/app/Domain/Timesheets/Templates/showAll.tpl.php index 6ccc41daec..b08fd27ab7 100644 --- a/app/Domain/Timesheets/Templates/showAll.tpl.php +++ b/app/Domain/Timesheets/Templates/showAll.tpl.php @@ -373,6 +373,18 @@ function restoreFilters() { + + + + get('invEmpl') == '1') { From 00e0ba1eae72322c92d75db86f684f029790ff92 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Thu, 26 Feb 2026 10:05:11 +0100 Subject: [PATCH 19/24] #6078 Add overflow to the whole table --- app/Domain/Timesheets/Templates/showAll.tpl.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Domain/Timesheets/Templates/showAll.tpl.php b/app/Domain/Timesheets/Templates/showAll.tpl.php index b08fd27ab7..e28a534d16 100644 --- a/app/Domain/Timesheets/Templates/showAll.tpl.php +++ b/app/Domain/Timesheets/Templates/showAll.tpl.php @@ -221,7 +221,7 @@ function restoreFilters() {
-
+
displayNotification() ?>