From 42cf9ade9bf85781252fc9409a2818a8f4c2aa25 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Sat, 14 Feb 2026 12:37:50 +0100 Subject: [PATCH 01/13] #5923 Add initial logic for refreshing to appropriate date --- .../js/app/timesheets/filterPreferences.js | 147 +++++++++++++++--- 1 file changed, 125 insertions(+), 22 deletions(-) diff --git a/public/assets/js/app/timesheets/filterPreferences.js b/public/assets/js/app/timesheets/filterPreferences.js index d19ce22a94..defd776adc 100644 --- a/public/assets/js/app/timesheets/filterPreferences.js +++ b/public/assets/js/app/timesheets/filterPreferences.js @@ -22,6 +22,7 @@ initUI(); initDateRangeTracking(); updateActiveProfileDisplay(); + checkAndRefreshActiveProfile(); } }; @@ -29,6 +30,70 @@ setTimeout(checkAndInit, 100); } + function checkAndRefreshActiveProfile() { + const activeProfile = localStorage.getItem('activeProfileName'); + const dateRange = localStorage.getItem('activeProfileDateRange'); + const lastApplied = localStorage.getItem('activeProfileLastApplied'); + + if (!activeProfile || !dateRange || !lastApplied || dateRange === 'Custom') { + return; + } + + const lastAppliedDate = new Date(lastApplied); + const now = new Date(); + + if (shouldRefreshRange(dateRange, lastAppliedDate, now)) { + + loadAllPreferences().then(function () { + const pref = currentPreferences[activeProfile]; + if (pref && pref.filters) { + applyFilters(pref.filters).then(function () { + localStorage.setItem('activeProfileLastApplied', now.toISOString()); + jQuery('#form').submit(); + }); + } + }); + } + } + + function shouldRefreshRange(rangeType, lastApplied, now) { + const lastAppliedDay = new Date(lastApplied.getFullYear(), lastApplied.getMonth(), lastApplied.getDate()); + const nowDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + switch (rangeType) { + case 'Today': + case 'Yesterday': + return lastAppliedDay.getTime() !== nowDay.getTime(); + + case 'Last 7 Days': + case 'Last 30 Days': + return lastAppliedDay.getTime() !== nowDay.getTime(); + + case 'This Week': + const lastWeekStart = getStartOfWeek(lastApplied); + const nowWeekStart = getStartOfWeek(now); + return lastWeekStart.getTime() !== nowWeekStart.getTime(); + + case 'This Month': + return lastApplied.getMonth() !== now.getMonth() || + lastApplied.getFullYear() !== now.getFullYear(); + + case 'Last Month': + return lastApplied.getMonth() !== now.getMonth() || + lastApplied.getFullYear() !== now.getFullYear(); + + default: + return false; + } + } + + function getStartOfWeek(date) { + const d = new Date(date); + const day = d.getDay(); + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + return new Date(d.setDate(diff)); + } + function initDateRangeTracking() { const dateInput = jQuery('input[name="dateFrom"]'); @@ -65,6 +130,8 @@ await applyFilters(data.preference.filters); activeProfileName = name; localStorage.setItem('activeProfileName', name); + localStorage.setItem('activeProfileDateRange', data.preference.filters.dateRange || 'Custom'); + localStorage.setItem('activeProfileLastApplied', new Date().toISOString()); updateActiveProfileDisplay(); jQuery('#form').submit(); return true; @@ -95,6 +162,8 @@ function clearActiveProfile() { activeProfileName = null; localStorage.removeItem('activeProfileName'); + localStorage.removeItem('activeProfileDateRange'); + localStorage.removeItem('activeProfileLastApplied'); updateActiveProfileDisplay(); } @@ -153,35 +222,69 @@ alert('Failed to save profile'); return false; } - function applyDateRange(rangeName) { - const dateInput = jQuery('input[name="dateFrom"]'); +function applyDateRange(rangeName) { + const dateInput = jQuery('input[name="dateFrom"]'); - if (!dateInput.length || !dateInput.data('daterangepicker')) { - return false; - } - - const picker = dateInput.data('daterangepicker'); - const ranges = picker.ranges; + if (!dateInput.length || !dateInput.data('daterangepicker')) { + console.log('[applyDateRange] Datepicker not found'); + return false; + } - if (ranges && ranges[rangeName]) { - const range = ranges[rangeName]; - const startDate = range[0]; - const endDate = range[1]; + const picker = dateInput.data('daterangepicker'); + + // Force fresh moment calculation - don't use picker.ranges + let startDate, endDate; + + switch(rangeName) { + case 'Today': + startDate = moment().startOf('day'); + endDate = moment().endOf('day'); + break; + case 'Yesterday': + startDate = moment().subtract(1, 'day').startOf('day'); + endDate = moment().subtract(1, 'day').endOf('day'); + break; + case 'This Week': + startDate = moment().startOf('week'); + endDate = moment().endOf('week'); + break; + case 'This Month': + startDate = moment().startOf('month'); + endDate = moment().endOf('month'); + break; + case 'Last 7 Days': + startDate = moment().subtract(6, 'days').startOf('day'); + endDate = moment().endOf('day'); + break; + case 'Last 30 Days': + startDate = moment().subtract(29, 'days').startOf('day'); + endDate = moment().endOf('day'); + break; + case 'Last Month': + startDate = moment().subtract(1, 'month').startOf('month'); + endDate = moment().subtract(1, 'month').endOf('month'); + break; + default: + console.log('[applyDateRange] Unknown range:', rangeName); + return false; + } - picker.setStartDate(startDate); - picker.setEndDate(endDate); - picker.chosenLabel = rangeName; + console.log('[applyDateRange] Calculated dates for', rangeName, ':', { + start: startDate.format('YYYY-MM-DD'), + end: endDate.format('YYYY-MM-DD') + }); - jQuery('input[name="dateFrom"]').val(startDate.format('YYYY-MM-DD')); - jQuery('input[name="dateTo"]').val(endDate.format('YYYY-MM-DD')); + picker.setStartDate(startDate); + picker.setEndDate(endDate); + picker.chosenLabel = rangeName; - selectedRangeName = rangeName; + jQuery('input[name="dateFrom"]').val(startDate.format('YYYY-MM-DD')); + jQuery('input[name="dateTo"]').val(endDate.format('YYYY-MM-DD')); - return true; - } + selectedRangeName = rangeName; - return false; - } + return true; +} async function applyFilters(filters) { if (!filters) { From e1ea8073710ce6c2033fee35ca9622db27f8b52b Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Fri, 13 Feb 2026 15:49:59 +0100 Subject: [PATCH 02/13] #5923 Add dynamic updates to today and this month labels in datepicker --- .../js/app/timesheets/filterPreferences.js | 185 +++++++----------- 1 file changed, 75 insertions(+), 110 deletions(-) diff --git a/public/assets/js/app/timesheets/filterPreferences.js b/public/assets/js/app/timesheets/filterPreferences.js index defd776adc..58f2268e30 100644 --- a/public/assets/js/app/timesheets/filterPreferences.js +++ b/public/assets/js/app/timesheets/filterPreferences.js @@ -65,10 +65,6 @@ case 'Yesterday': return lastAppliedDay.getTime() !== nowDay.getTime(); - case 'Last 7 Days': - case 'Last 30 Days': - return lastAppliedDay.getTime() !== nowDay.getTime(); - case 'This Week': const lastWeekStart = getStartOfWeek(lastApplied); const nowWeekStart = getStartOfWeek(now); @@ -98,13 +94,21 @@ const dateInput = jQuery('input[name="dateFrom"]'); if (dateInput.length && dateInput.data('daterangepicker')) { + const picker = dateInput.data('daterangepicker'); + jQuery(document).on('click', '.daterangepicker .ranges li', function () { + const label = jQuery(this).text().trim(); + selectedRangeName = label; + picker._storedChosenLabel = label; + }); + dateInput.on('apply.daterangepicker', function (ev, picker) { - if (picker.chosenLabel) { + if (picker.chosenLabel && picker.chosenLabel !== 'Custom Range') { selectedRangeName = picker.chosenLabel; + picker._storedChosenLabel = picker.chosenLabel; } }); } else { - setTimeout(initDateRangeTracking, 500); + setTimeout(initDateRangeTracking, 300); } } @@ -222,83 +226,52 @@ alert('Failed to save profile'); return false; } -function applyDateRange(rangeName) { - const dateInput = jQuery('input[name="dateFrom"]'); + function applyDateRange(rangeName) { - if (!dateInput.length || !dateInput.data('daterangepicker')) { - console.log('[applyDateRange] Datepicker not found'); - return false; - } - - const picker = dateInput.data('daterangepicker'); - - // Force fresh moment calculation - don't use picker.ranges - let startDate, endDate; - - switch(rangeName) { - case 'Today': - startDate = moment().startOf('day'); - endDate = moment().endOf('day'); - break; - case 'Yesterday': - startDate = moment().subtract(1, 'day').startOf('day'); - endDate = moment().subtract(1, 'day').endOf('day'); - break; - case 'This Week': - startDate = moment().startOf('week'); - endDate = moment().endOf('week'); - break; - case 'This Month': - startDate = moment().startOf('month'); - endDate = moment().endOf('month'); - break; - case 'Last 7 Days': - startDate = moment().subtract(6, 'days').startOf('day'); - endDate = moment().endOf('day'); - break; - case 'Last 30 Days': - startDate = moment().subtract(29, 'days').startOf('day'); - endDate = moment().endOf('day'); - break; - case 'Last Month': - startDate = moment().subtract(1, 'month').startOf('month'); - endDate = moment().subtract(1, 'month').endOf('month'); - break; - default: - console.log('[applyDateRange] Unknown range:', rangeName); + const dateInput = jQuery('input[name="dateFrom"]'); + if (!dateInput.length || !dateInput.data('daterangepicker')) { return false; - } + } + + const picker = dateInput.data('daterangepicker'); + + let startDate, endDate; - console.log('[applyDateRange] Calculated dates for', rangeName, ':', { - start: startDate.format('YYYY-MM-DD'), - end: endDate.format('YYYY-MM-DD') - }); + switch (rangeName) { + case 'Today': + startDate = moment().startOf('day'); + endDate = moment().endOf('day'); + break; + case 'This Week': + startDate = moment().startOf('isoWeek'); + endDate = moment().endOf('isoWeek'); + break; + case 'This Month': + startDate = moment().startOf('month'); + endDate = moment().endOf('month'); + break; + default: + return false; + } - picker.setStartDate(startDate); - picker.setEndDate(endDate); - picker.chosenLabel = rangeName; + picker.setStartDate(startDate); + picker.setEndDate(endDate); - jQuery('input[name="dateFrom"]').val(startDate.format('YYYY-MM-DD')); - jQuery('input[name="dateTo"]').val(endDate.format('YYYY-MM-DD')); + picker.chosenLabel = rangeName; + picker._storedChosenLabel = rangeName; - selectedRangeName = rangeName; + jQuery('input[name="dateFrom"]').val(startDate.format('YYYY-MM-DD')); + jQuery('input[name="dateTo"]').val(endDate.format('YYYY-MM-DD')); - return true; -} + selectedRangeName = rangeName; + + return true; + } async function applyFilters(filters) { if (!filters) { return; } - if (filters.clientId) { - jQuery('select[name="clientId"]').val(filters.clientId); - } - if (filters.userId) { - jQuery('select[name="userId"]').val(filters.userId); - } - if (filters.kind) { - jQuery('select[name="kind"]').val(filters.kind); - } if (filters.dateRange && filters.dateRange !== 'Custom') { const applied = applyDateRange(filters.dateRange); @@ -319,41 +292,6 @@ function applyDateRange(rangeName) { jQuery('input[name="dateTo"]').val(filters.dateTo); } } - - jQuery('input[name="invEmpl"]').prop('checked', filters.invEmpl === '1'); - jQuery('input[name="invComp"]').prop('checked', filters.invComp === '1'); - jQuery('input[name="paid"]').prop('checked', filters.paid === '1'); - - if (filters.projects && Array.isArray(filters.projects)) { - jQuery('input[name="project[]"]').prop('checked', false); - - if (filters.projects.includes('-1') || filters.projects.length === 0) { - jQuery('#projectCheckboxAll').prop('checked', true); - } else { - filters.projects.forEach(function (projectId) { - jQuery('input[name="project[]"][value="' + projectId + '"]').prop('checked', true); - }); - jQuery('#projectCheckboxAll').prop('checked', false); - } - - if (typeof updateProjectCountInline === 'function') { - updateProjectCountInline(); - } - } - - if (filters.columnState && dataTableInstance && typeof dataTableInstance.columns === 'function') { - dataTableInstance.columns().every(function (index) { - const column = this; - const columnName = jQuery(column.header()).data('column-name'); - if (columnName && filters.columnState.hasOwnProperty(columnName)) { - column.visible(filters.columnState[columnName]); - } - }); - - if (typeof window.leantimeDataTablesColumnState !== 'undefined') { - await window.leantimeDataTablesColumnState.save('allTimesheetsTable', filters.columnState, true); - } - } } async function loadAllPreferences() { @@ -389,10 +327,14 @@ function applyDateRange(rangeName) { if (dateInput.length && dateInput.data('daterangepicker')) { const picker = dateInput.data('daterangepicker'); - if (picker.chosenLabel) { + if (picker._storedChosenLabel && picker._storedChosenLabel !== 'Custom Range') { + dateRange = picker._storedChosenLabel; + } else if (picker.chosenLabel && picker.chosenLabel !== 'Custom Range') { dateRange = picker.chosenLabel; - } else if (selectedRangeName) { + } else if (selectedRangeName && selectedRangeName !== 'Custom Range') { dateRange = selectedRangeName; + } else { + dateRange = detectRangeFromDates(dateFrom, dateTo); } } @@ -443,6 +385,31 @@ function applyDateRange(rangeName) { return projectFilters; } + function detectRangeFromDates(dateFrom, dateTo) { + if (!dateFrom || !dateTo) return 'Custom'; + + const from = moment(dateFrom, 'MM/DD/YYYY'); + const to = moment(dateTo, 'MM/DD/YYYY'); + const today = moment().startOf('day'); + + if (from.isSame(today, 'day') && to.isSame(today, 'day')) { + return 'Today'; + } + + const thisWeekStart = moment().startOf('isoWeek'); + const thisWeekEnd = moment().endOf('isoWeek'); + if (from.isSame(thisWeekStart, 'day') && to.isSame(thisWeekEnd, 'day')) { + return 'This Week'; + } + + const thisMonthStart = moment().startOf('month'); + const thisMonthEnd = moment().endOf('month'); + if (from.isSame(thisMonthStart, 'day') && to.isSame(thisMonthEnd, 'day')) { + return 'This Month'; + } + + return 'Custom'; + } async function deletePreference(name) { @@ -621,13 +588,11 @@ function applyDateRange(rangeName) { } function showProjectSelector(profileName) { - // Remove existing modal if any jQuery('#slackProjectModal').remove(); const pref = currentPreferences[profileName]; const currentProjectId = pref ? pref.slackProjectId : ''; - // Build project options let projectOptions = ''; if (typeof projectNames !== 'undefined') { for (const [id, name] of Object.entries(projectNames)) { From 4d9b12b33fb1eeab050468493de1a507888c352c Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:44:19 +0100 Subject: [PATCH 03/13] #5466 Improve Export in My Timesheets --- .idea/leantime-oss.iml | 2 +- .../Timesheets/Controllers/ShowMyList.php | 72 ++++++-- .../Timesheets/Js/timesheetsController.js | 12 +- .../Timesheets/Templates/showMyList.tpl.php | 155 +++++++++++++----- .../Templates/showMyMonthlyTimesheet.tpl.php | 3 +- public/assets/css/components/tables.css | 43 +++++ 6 files changed, 224 insertions(+), 63 deletions(-) diff --git a/.idea/leantime-oss.iml b/.idea/leantime-oss.iml index 7c0ff9f072..fc8d27cf07 100644 --- a/.idea/leantime-oss.iml +++ b/.idea/leantime-oss.iml @@ -124,4 +124,4 @@ - \ No newline at end of file + diff --git a/app/Domain/Timesheets/Controllers/ShowMyList.php b/app/Domain/Timesheets/Controllers/ShowMyList.php index a882c7f8fc..5a7ac34be1 100644 --- a/app/Domain/Timesheets/Controllers/ShowMyList.php +++ b/app/Domain/Timesheets/Controllers/ShowMyList.php @@ -6,6 +6,8 @@ use Leantime\Core\Controller\Controller; use Leantime\Domain\Auth\Models\Roles; use Leantime\Domain\Auth\Services\Auth; +use Leantime\Domain\Clients\Services\Clients as ClientService; +use Leantime\Domain\Projects\Services\Projects as ProjectService; use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService; use Symfony\Component\HttpFoundation\Response; @@ -13,9 +15,15 @@ class ShowMyList extends Controller { private TimesheetService $timesheetService; - public function init(TimesheetService $timesheetService): void + private ProjectService $projectService; + + private ClientService $clientService; + + public function init(TimesheetService $timesheetService, ProjectService $projectService, ClientService $clientService): void { $this->timesheetService = $timesheetService; + $this->projectService = $projectService; + $this->clientService = $clientService; session(['lastPage' => BASE_URL.'/timesheets/showMyList']); } @@ -31,15 +39,38 @@ public function run(): Response $kind = 'all'; if (! empty($_POST['kind'])) { - $kind = ($_POST['kind']); + $kind = (string) $_POST['kind']; } - // Use UTC here as all data stored in the database should be UTC (start in user's timezone and convert to UTC). - // The front end javascript is hardcode to start the week on mondays, so we use that here too. + $projectId = -1; + if (isset($_POST['projectId']) && $_POST['projectId'] !== '' && $_POST['projectId'] !== 'all') { + $projectId = (int) $_POST['projectId']; + } - // Get start of the week in current users timezone and then switch to UTC - $dateFrom = dtHelper()->userNow()->startOfWeek(CarbonInterface::MONDAY)->setToDbTimezone(); - $dateTo = dtHelper()->userNow()->endOfWeek()->setToDbTimezone(); + $clientId = -1; + if (isset($_POST['clientId']) && $_POST['clientId'] !== '' && $_POST['clientId'] !== '-1') { + $clientId = (int) $_POST['clientId']; + } + + $invEmplCheck = '-1'; + if (isset($_POST['invEmpl']) && $_POST['invEmpl'] === '1') { + $invEmplCheck = '1'; + } + + $invCompCheck = '0'; + if (isset($_POST['invComp'])) { + $invCompCheck = $_POST['invComp'] === 'on' || $_POST['invComp'] === '1' ? '1' : '0'; + } + + $paidCheck = '0'; + if (isset($_POST['paid'])) { + $paidCheck = ($_POST['paid'] === 'on' || $_POST['paid'] === '1') ? '1' : '0'; + } + + // Use UTC here as all data stored in the database should be UTC (start in user's timezone and convert to UTC). + // Default view: This Month (start and end of current month in user's timezone). + $dateFrom = dtHelper()->userNow()->startOfMonth()->setToDbTimezone(); + $dateTo = dtHelper()->userNow()->endOfMonth()->setToDbTimezone(); if (! empty($_POST['dateFrom'])) { $dateFrom = dtHelper()->parseUserDateTime($_POST['dateFrom'])->setToDbTimezone(); @@ -49,19 +80,36 @@ public function run(): Response $dateTo = dtHelper()->parseUserDateTime($_POST['dateTo'])->setToDbTimezone(); } + $userId = session('userdata.id'); + $allProjects = $this->projectService->getProjectsUserHasAccessTo($userId); + if (! is_array($allProjects)) { + $allProjects = []; + } + + $allClients = $this->clientService->getAll(); + $this->tpl->assign('dateFrom', $dateFrom); $this->tpl->assign('dateTo', $dateTo); $this->tpl->assign('actKind', $kind); + $this->tpl->assign('projectFilter', $projectId); + $this->tpl->assign('allProjects', $allProjects); + $this->tpl->assign('clientFilter', $clientId); + $this->tpl->assign('allClients', $allClients); + $this->tpl->assign('invEmpl', $invEmplCheck); + $this->tpl->assign('invComp', $invCompCheck); + $this->tpl->assign('paid', $paidCheck); $this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes()); $this->tpl->assign('allTimesheets', $this->timesheetService->getAll( dateFrom: $dateFrom, dateTo: $dateTo, - projectId: -1, + projectId: $projectId, kind: $kind, - userId: session('userdata.id'), - invEmpl: -1, - invComp: -1, - paid: -1 + userId: $userId, + invEmpl: $invEmplCheck, + invComp: $invCompCheck, + paid: $paidCheck, + ticketFilter: '-1', + clientId: (string) $clientId )); // Pass user's hours format preference to template for CSV export diff --git a/app/Domain/Timesheets/Js/timesheetsController.js b/app/Domain/Timesheets/Js/timesheetsController.js index 6141ff8f2e..f662e78a56 100644 --- a/app/Domain/Timesheets/Js/timesheetsController.js +++ b/app/Domain/Timesheets/Js/timesheetsController.js @@ -113,24 +113,24 @@ document.addEventListener('click', function(e) { if (target && !isReady) { e.preventDefault(); e.stopPropagation(); - + target.style.opacity = '0.5'; target.style.cursor = 'wait'; - + pendingClick = target; } }, true); function initWhenReady() { - + if (typeof jQuery.fn.nyroModal === 'undefined') { setTimeout(initWhenReady, 100); return; } - + initEditTimeModal(); isReady = true; - + if (pendingClick) { var $pending = jQuery(pendingClick); pendingClick.style.opacity = ''; @@ -166,7 +166,7 @@ var initEditTimeModal = function () { }; jQuery(document).on('click', '.editTimeModal', function (e) { - if (!isReady) return; + if (!isReady) return; e.preventDefault(); jQuery(this).nyroModal(canvasoptions); }); diff --git a/app/Domain/Timesheets/Templates/showMyList.tpl.php b/app/Domain/Timesheets/Templates/showMyList.tpl.php index 83d9ec7f2c..11d07f9e20 100644 --- a/app/Domain/Timesheets/Templates/showMyList.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyList.tpl.php @@ -8,7 +8,44 @@ } $hoursFormat = session('usersettings.hours_format', 'decimal'); ?> + + + + +