From 11ad6938aa5ae285f45b489a92700c7ed4eb6a2a Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:25:08 +0100 Subject: [PATCH 01/28] #5427 Add visual indicator for pinned ticket --- app/Domain/Tickets/Templates/showKanban.tpl.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Domain/Tickets/Templates/showKanban.tpl.php b/app/Domain/Tickets/Templates/showKanban.tpl.php index d543e274e2..56c4a98864 100644 --- a/app/Domain/Tickets/Templates/showKanban.tpl.php +++ b/app/Domain/Tickets/Templates/showKanban.tpl.php @@ -268,7 +268,12 @@ class="ticketBox moveable container priority-border-" __('label.'.strtolower($row['type'])); ?>
-

e($row['headline']); ?>

+

+ + + + e($row['headline']); ?> +

escapeMinimalRemoveImage($row['description']); ?> From 98bc24f44892d09d272c102532cf2de532019735 Mon Sep 17 00:00:00 2001 From: farukvukovic <104840615+farukvukovic@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:10:10 +0100 Subject: [PATCH 02/28] #5343 disable profile date saving --- .../js/app/timesheets/filterPreferences.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/public/assets/js/app/timesheets/filterPreferences.js b/public/assets/js/app/timesheets/filterPreferences.js index 2f6a9f7587..2e87b66e46 100644 --- a/public/assets/js/app/timesheets/filterPreferences.js +++ b/public/assets/js/app/timesheets/filterPreferences.js @@ -119,12 +119,7 @@ jQuery('select[name="kind"]').val(filters.kind); } - if (filters.dateFrom) { - jQuery('input[name="dateFrom"]').val(filters.dateFrom); - } - if (filters.dateTo) { - 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'); @@ -192,8 +187,6 @@ clientId: jQuery('select[name="clientId"]').val() || '-1', userId: jQuery('select[name="userId"]').val() || 'all', kind: jQuery('select[name="kind"]').val() || 'all', - dateFrom: jQuery('input[name="dateFrom"]').val() || '', - dateTo: jQuery('input[name="dateTo"]').val() || '', invEmpl: jQuery('input[name="invEmpl"]').is(':checked') ? '1' : '0', invComp: jQuery('input[name="invComp"]').is(':checked') ? '1' : '0', paid: jQuery('input[name="paid"]').is(':checked') ? '1' : '0' @@ -285,7 +278,7 @@ function buildPreferencesDropdown() { keys.forEach(function (key) { const pref = currentPreferences[key]; const checked = pref.autoExport ? 'checked' : ''; - + html += `
${key}
@@ -304,7 +297,7 @@ function buildPreferencesDropdown() { } function initAutoExportListeners() { - jQuery(document).on('change', '.auto-export', function() { + jQuery(document).on('change', '.auto-export', function() { fetch(leantime.appUrl + '/timesheets/saveFilterPreferences', { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -449,7 +442,7 @@ jQuery(document).ready(initAutoExportListeners); e.stopPropagation(); const profileName = jQuery(this).data('name'); const isEnabled = jQuery(this).is(':checked'); - + saveAutoExportSetting(profileName, isEnabled); }); @@ -470,7 +463,7 @@ jQuery(document).ready(initAutoExportListeners); autoExport: enabled }) }); - + if (response.ok) { const data = await response.json(); if (data.status === 'success') { From d67cfa492bc84da008c499475743fd548f677ee8 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 6 Jan 2026 15:50:21 +0100 Subject: [PATCH 03/28] #5418 Add initial logic for navigating to monthly view through button --- .../Controllers/ShowMyMonthlyTimesheet.php | 170 ++++++++++++ .../Timesheets/Repositories/Timesheets.php | 62 +++++ app/Domain/Timesheets/Services/Timesheets.php | 4 + .../Timesheets/Templates/showMy.tpl.php | 1 + .../Timesheets/Templates/showMyList.tpl.php | 1 + .../Templates/showMyMonthlyTimesheet.tpl.php | 245 ++++++++++++++++++ app/Language/en-US.ini | 1 + 7 files changed, 484 insertions(+) create mode 100644 app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php create mode 100644 app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php diff --git a/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php b/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php new file mode 100644 index 0000000000..44f7fda37f --- /dev/null +++ b/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php @@ -0,0 +1,170 @@ +timesheetService = $timesheetService; + $this->timesheetRepo = $timesheetRepo; + $this->projects = $projects; + $this->tickets = $tickets; + $this->userRepo = $userRepo; + } + + /** + * run - display template and edit data + * + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function run(): Response + { + Auth::authOrRedirect([Roles::$owner, Roles::$admin, Roles::$manager, Roles::$editor], true); + + $fromDate = dtHelper()->userNow()->startOfMonth()->setToDbTimezone(); + + $kind = 'all'; + if (isset($_POST['search'])) { + // User date comes is in user date format and user timezone. Change it to utc. + if (! empty($_POST['startDate'])) { + try { + $fromDate = dtHelper()->parseUserDateTime($_POST['startDate'])->setToDbTimezone(); + } catch (\Exception $e) { + Log::warning($e); + Log::warning('User timezone: '.session('usersettings.timezone')); + Log::warning('User dateTime format: '.session('usersettings.date_format')); + $this->tpl->setNotification('Could not parse date', 'error', 'save_timesheet'); + } + } + } + + if (isset($_POST['saveTimeSheet'])) { + $this->saveTimeSheet($_POST); + } + + $myTimesheets = $this->timesheetService->getWeeklyTimesheets(-1, $fromDate, session('userdata.id')); + $existingTicketIds = array_map(fn ($item) => $item['ticketId'], $myTimesheets); + + $this->tpl->assign('existingTicketIds', $existingTicketIds); + $this->tpl->assign('dateFrom', $fromDate); + $this->tpl->assign('actKind', $kind); + $this->tpl->assign('kind', $this->timesheetRepo->kind); + $this->tpl->assign('allProjects', $this->projects->getUserProjects( + userId: session('userdata.id'), + projectTypes: 'project' + )); + $this->tpl->assign('allTickets', $this->tickets->getUsersTickets( + id: session('userdata.id'), + limit: -1 + )); + $this->tpl->assign('allTimesheets', $myTimesheets); + + return $this->tpl->display('timesheets.showMyMonthlyTimesheet'); + } + + /** + * @throws BindingResolutionException + */ + public function saveTimeSheet(array $postData): void + { + foreach ($postData as $key => $dateEntry) { + // The temp data should contain four parts, spectated by "|": + // TICKET ID | Type of booked hours | Date | Timestamp + $tempData = explode('|', $key); + + if (count($tempData) === 4) { + $ticketId = $tempData[0]; + $kind = $tempData[1]; + $date = $tempData[2]; + $timestamp = $tempData[3]; + $hours = $dateEntry; + + // if ticket ID is set to new, pull id and hour type from form field + if ($ticketId === 'new' || $ticketId === 0) { + $ticketId = (int) $postData['ticketId']; + $kind = $postData['kindId']; + + if ($ticketId == 0 && $hours > 0) { + $this->tpl->setNotification('Task ID is required for new entries', 'error', 'save_timesheet'); + + return; + } + } + + // Parse hours using TimeParser for flexible input formats + $parsedHours = $hours; + if (!empty($hours) && !is_numeric($hours)) { + try { + $parser = app(\Leantime\Domain\Timesheets\Services\TimeParser::class); + $parsedHours = $parser->parseTimeToDecimal($hours); + + // Additional validation: check for unreasonably large values + if ($parsedHours > 24 * 365) { // More than a year of work + throw new \InvalidArgumentException('Time value is unreasonably large. Please enter a valid amount of time.'); + } + } catch (\InvalidArgumentException $e) { + $this->tpl->setNotification($e->getMessage(), 'error', 'time_parse_error'); + + // Skip saving this entry if parsing failed + continue; + } + } + + $values = [ + 'userId' => session('userdata.id'), + 'ticket' => $ticketId, + 'date' => $date, + 'timestamp' => $timestamp, + 'hours' => $parsedHours, + 'kind' => $kind, + ]; + + // This should not be the case since we set the input to disabled, but check anyways + if ($timestamp !== 'false' && $timestamp != false) { + try { + $this->timesheetService->upsertTime($ticketId, $values); + $this->tpl->setNotification('Timesheet saved successfully', 'success', 'save_timesheet'); + } catch (\Exception $e) { + $this->tpl->setNotification('Error logging time: '.$e->getMessage(), 'error', 'save_timesheet'); + report($e); + + continue; + } + } + } + } + } +} diff --git a/app/Domain/Timesheets/Repositories/Timesheets.php b/app/Domain/Timesheets/Repositories/Timesheets.php index d46465a9a2..001e01ef89 100644 --- a/app/Domain/Timesheets/Repositories/Timesheets.php +++ b/app/Domain/Timesheets/Repositories/Timesheets.php @@ -354,6 +354,68 @@ public function getWeeklyTimesheets(int $projectId, CarbonInterface $fromDate, i return $call->fetchAll(); } + public function getMonthlyTimesheets(int $projectId, CarbonInterface $fromDate, int $userId = 0): mixed + { + $query = 'SELECT + zp_timesheets.id, + zp_timesheets.userId, + zp_timesheets.ticketId, + zp_timesheets.workDate as workDate, + zp_timesheets.hours, + zp_timesheets.description, + zp_timesheets.kind, + zp_timesheets.invoicedEmpl, + zp_timesheets.invoicedComp, + zp_timesheets.invoicedEmplDate, + zp_timesheets.invoicedCompDate, + zp_timesheets.paid, + zp_timesheets.paidDate, + zp_timesheets.kind, + zp_timesheets.modified, + zp_tickets.headline, + zp_tickets.planHours, + zp_projects.name, + zp_projects.id AS projectId, + zp_projects.clientId AS clientId, + zp_clients.name AS clientName + FROM + zp_timesheets + LEFT JOIN zp_tickets ON zp_tickets.id = zp_timesheets.ticketId + LEFT JOIN zp_projects ON zp_tickets.projectId = zp_projects.id + LEFT JOIN zp_clients ON zp_clients.id = zp_projects.clientId + WHERE + (zp_timesheets.workDate >= :dateStart1 AND zp_timesheets.workDate < :dateEnd) + AND (zp_timesheets.userId = :userId) + AND hours > 0 + '; + + if ($projectId > 0) { + $query .= ' AND zp_tickets.projectId = :projectId'; + } + + $query .= ' ORDER BY zp_timesheets.ticketId, zp_timesheets.kind, zp_timesheets.workDate DESC'; + + $call = $this->dbcall(func_get_args()); + + $call->prepare($query); + + if (! $fromDate->isUtc()) { + $fromDate->setTimezone('UTC'); + } + + $call->bindValue(':dateStart1', $fromDate); + + $endDate = $fromDate->addDays(7); + $call->bindValue(':dateEnd', $endDate); + $call->bindValue(':userId', $userId, PDO::PARAM_INT); + + if ($projectId > 0) { + $call->bindValue(':projectId', $projectId, PDO::PARAM_INT); + } + + return $call->fetchAll(); + } + /** * getUsersTicketHours - get the total hours * diff --git a/app/Domain/Timesheets/Services/Timesheets.php b/app/Domain/Timesheets/Services/Timesheets.php index c4ac0fe2bf..3f106b283f 100644 --- a/app/Domain/Timesheets/Services/Timesheets.php +++ b/app/Domain/Timesheets/Services/Timesheets.php @@ -593,4 +593,8 @@ private function prepareDatesForApiResponse($timesheet) return $timesheet; } + + public function getMonthlyTimesheets (CarbonInterface $dateFrom) { + + } } diff --git a/app/Domain/Timesheets/Templates/showMy.tpl.php b/app/Domain/Timesheets/Templates/showMy.tpl.php index e24df082d7..099dbcd26e 100644 --- a/app/Domain/Timesheets/Templates/showMy.tpl.php +++ b/app/Domain/Timesheets/Templates/showMy.tpl.php @@ -229,6 +229,7 @@
diff --git a/app/Domain/Timesheets/Templates/showMyList.tpl.php b/app/Domain/Timesheets/Templates/showMyList.tpl.php index 2c3e340280..b23e019bce 100644 --- a/app/Domain/Timesheets/Templates/showMyList.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyList.tpl.php @@ -75,6 +75,7 @@ class="dateTo"
diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php new file mode 100644 index 0000000000..ecff019f29 --- /dev/null +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -0,0 +1,245 @@ + $val) { + $$var = $val; // necessary for blade refactor +} +$hoursFormat = session('usersettings.hours_format', 'decimal'); +?> + + + + + + +
+
+ displayNotification(); +?> + +
+
+ __('links.filter') ?> (1) + +
+
+
+ + +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + get('allTimesheets') as $row) { + $sum = $sum + $row['hours']; ?> + + + + e($row['hours']); ?>">e($row['hours'] ?: 0); ?> */ ?> + + e($row['planHours']); ?>">e($row['planHours'] ?: 0); ?> */ ?> + + + "> */ ?> + + + + + + + + + + + + + + + + + + */ ?> + + + +
__('label.id'); ?>__('label.date'); ?>__('label.hours'); ?>__('label.plan_hours'); ?>__('label.difference'); ?>__('label.ticket'); ?>__('label.project'); ?>__('label.employee'); ?>__('label.type')?>__('label.description'); ?>__('label.invoiced'); ?>__('label.invoiced_comp'); ?>__('label.paid'); ?>
+ #__('label.edit'); ?> + date(); ?> + time(); ?> + + + + + + + + e($row['headline']); ?> + + e($row['name']); ?> + + __('text.full_name'), $tpl->escape($row['firstname']), $tpl->escape($row['lastname'])); ?> + + __($tpl->get('kind')[$row['kind']]); ?> + + e($row['description']); ?> + + date(); + } else { + echo $tpl->__('label.pending'); + } ?> + + date(); + } else { + echo $tpl->__('label.pending'); + } ?> + + date(); + } else { + echo $tpl->__('label.pending'); + } ?> +
__('label.total_hours')?>
+
+
+
+ + diff --git a/app/Language/en-US.ini b/app/Language/en-US.ini index 33a30093c1..eada441df2 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -466,6 +466,7 @@ links.create_first_sprint = " Create your firs links.kanban = " Kanban" links.list = " List" links.week_view = " Week View" +links.monthly_view = " Monthly View" links.list_view = " List View" links.backlog = "Backlog" links.table = " Table" From 5980f8edf94d9f498c68f6df4e20da5f3dc52905 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Tue, 6 Jan 2026 16:01:13 +0100 Subject: [PATCH 04/28] #5418 Add getMonthlyTimesheet method to timesheet repository --- .../Timesheets/Repositories/Timesheets.php | 17 ++++++----------- app/Domain/Timesheets/Services/Timesheets.php | 1 - 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/Domain/Timesheets/Repositories/Timesheets.php b/app/Domain/Timesheets/Repositories/Timesheets.php index 001e01ef89..c5a52e176a 100644 --- a/app/Domain/Timesheets/Repositories/Timesheets.php +++ b/app/Domain/Timesheets/Repositories/Timesheets.php @@ -354,8 +354,11 @@ public function getWeeklyTimesheets(int $projectId, CarbonInterface $fromDate, i return $call->fetchAll(); } - public function getMonthlyTimesheets(int $projectId, CarbonInterface $fromDate, int $userId = 0): mixed + public function getMonthlyTimesheets(int $projectId, int $userId = 0): mixed { + $fromDate = dtHelper()->userNow()->startOfMonth()->setToDbTimezone(); + $endDate = dtHelper()->userNow()->endOfMonth()->addDay()->setToDbTimezone(); + $query = 'SELECT zp_timesheets.id, zp_timesheets.userId, @@ -370,7 +373,6 @@ public function getMonthlyTimesheets(int $projectId, CarbonInterface $fromDate, zp_timesheets.invoicedCompDate, zp_timesheets.paid, zp_timesheets.paidDate, - zp_timesheets.kind, zp_timesheets.modified, zp_tickets.headline, zp_tickets.planHours, @@ -384,7 +386,7 @@ public function getMonthlyTimesheets(int $projectId, CarbonInterface $fromDate, LEFT JOIN zp_projects ON zp_tickets.projectId = zp_projects.id LEFT JOIN zp_clients ON zp_clients.id = zp_projects.clientId WHERE - (zp_timesheets.workDate >= :dateStart1 AND zp_timesheets.workDate < :dateEnd) + (zp_timesheets.workDate >= :dateStart AND zp_timesheets.workDate < :dateEnd) AND (zp_timesheets.userId = :userId) AND hours > 0 '; @@ -396,16 +398,9 @@ public function getMonthlyTimesheets(int $projectId, CarbonInterface $fromDate, $query .= ' ORDER BY zp_timesheets.ticketId, zp_timesheets.kind, zp_timesheets.workDate DESC'; $call = $this->dbcall(func_get_args()); - $call->prepare($query); - if (! $fromDate->isUtc()) { - $fromDate->setTimezone('UTC'); - } - - $call->bindValue(':dateStart1', $fromDate); - - $endDate = $fromDate->addDays(7); + $call->bindValue(':dateStart', $fromDate); $call->bindValue(':dateEnd', $endDate); $call->bindValue(':userId', $userId, PDO::PARAM_INT); diff --git a/app/Domain/Timesheets/Services/Timesheets.php b/app/Domain/Timesheets/Services/Timesheets.php index 3f106b283f..1e2bac825d 100644 --- a/app/Domain/Timesheets/Services/Timesheets.php +++ b/app/Domain/Timesheets/Services/Timesheets.php @@ -372,7 +372,6 @@ public function getWeeklyTimesheets(int $projectId, CarbonInterface $fromDate, i fromDate: $fromDate, userId: $userId ); - // Timesheets are grouped by ticketId + type $timesheetGroups = []; foreach ($allTimesheets as $timesheet) { From 64363509e14a00b7b5a30857f623491389d4fb69 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 7 Jan 2026 09:44:58 +0100 Subject: [PATCH 05/28] #5418 Change jquery ids --- .../Templates/showMyMonthlyTimesheet.tpl.php | 639 ++++++++++++------ 1 file changed, 429 insertions(+), 210 deletions(-) diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index ecff019f29..53fd2dcd0b 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -1,245 +1,464 @@ $val) { $$var = $val; // necessary for blade refactor } + +/** @var \Carbon\Carbon $currentDate */ +$dateFrom = $tpl->get('dateFrom'); $hoursFormat = session('usersettings.hours_format', 'decimal'); + ?> + -
displayNotification(); -?> -
-
- __('links.filter') ?> (1) - + +
+ +
-
-
- - +
+ +
+ + + __('label.until'); ?> + + +
-
-
-
+
+ + + + + + + + + + + + + -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - get('allTimesheets') as $row) { - $sum = $sum + $row['hours']; ?> - - - - e($row['hours']); ?>">e($row['hours'] ?: 0); ?> */ ?> - - e($row['planHours']); ?>">e($row['planHours'] ?: 0); ?> */ ?> - - - "> */ ?> - - - - - - - - - - - - - - - - - - */ ?> - - - -
__('label.id'); ?>__('label.date'); ?>__('label.hours'); ?>__('label.plan_hours'); ?>__('label.difference'); ?>__('label.ticket'); ?>__('label.project'); ?>__('label.employee'); ?>__('label.type')?>__('label.description'); ?>__('label.invoiced'); ?>__('label.invoiced_comp'); ?>__('label.paid'); ?>
- #__('label.edit'); ?> - date(); ?> - time(); ?> - - - - - - - - e($row['headline']); ?> - - e($row['name']); ?> - - __('text.full_name'), $tpl->escape($row['firstname']), $tpl->escape($row['lastname'])); ?> - - __($tpl->get('kind')[$row['kind']]); ?> - - e($row['description']); ?> - - date(); - } else { - echo $tpl->__('label.pending'); - } ?> - - date(); - } else { - echo $tpl->__('label.pending'); - } ?> - - date(); - } else { - echo $tpl->__('label.pending'); - } ?> -
__('label.total_hours')?>
- -
-
+ + + - // Initialize CSV export formatter (inline to avoid build dependency) - window.leantime = window.leantime || {}; - window.leantime.timesheetsExport = window.leantime.timesheetsExport || {}; - - if (typeof window.leantime.timesheetsExport.resolveCell !== 'function') { - window.leantime.timesheetsExport.resolveCell = function ($node, fallbackData) { - if (typeof $node.data('order') === 'undefined') { - return fallbackData; - } + $days = explode(',', $tpl->__('language.dayNamesShort')); + // Make the first day of week monday, by shifting sunday to the back of the array. + $days[] = array_shift($days); + ?> + + __('label.client_product') ?> + __('subtitles.todo') ?> + __('label.type') ?> + +
+ addDays($i)->formatDateForUser(); + $i++; + ?> + + + __('label.total') ?> + + + + 0, + 'day2' => 0, + 'day3' => 0, + 'day4' => 0, + 'day5' => 0, + 'day6' => 0, + 'day7' => 0, + ]; + // @todo: move all this calculations into the service the timesheets class. + foreach ($tpl->get('allTimesheets') as $timeRow) { + $timesheetId = 'new'; + ?> + + e($timeRow['clientName']); ?> // e($timeRow['name']); ?> + + e($timeRow['headline']); ?> + + + __($tpl->get('kind')[$timeRow['kind'] ?? 'GENERAL_BILLABLE'] ?? $tpl->get('kind')['GENERAL_BILLABLE']); ?> + + + + + - var tableFormat = ($node.closest('table[data-hours-format]').data('hoursFormat') || '').toString(); + - if (tableFormat === 'human') { - // jQuery converts data-export-display to exportDisplay in .data() - if (typeof $node.data('exportDisplay') !== 'undefined') { - return $node.data('exportDisplay'); - } + - return $node.text().trim(); - } - return $node.data('order'); - }; - } + formatDateForUser() : 'false') . '|' . ($timeRow[$dayKey]['actualWorkDate'] ? $timeRow[$dayKey]['actualWorkDate']->getTimestamp() : 'false'); + ?> + + name="" + value="" + data-decimal-value="" + + data-tippy-content="Cannot add time entry in previous timezone" + /> - jQuery(document).ready(function(){ - leantime.timesheetsController.initTimesheetsTable(); - leantime.timesheetsController.initEditTimeModal(); - leantime.dateController.initDateRangePicker(".dateFrom", ".dateTo", 1); - }); - + + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + __('label.total') ?> + $col) { + $totalHours += $col; + + ?> + + + + + + +
+
+ +
+
+ +
+
\ No newline at end of file From 09c81728724769566707f4e0e83b815f08136fef Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 7 Jan 2026 10:32:04 +0100 Subject: [PATCH 06/28] #5418 Fix logic for dynamically changing months so user can navigate through every month of every year --- .../Controllers/ShowMyMonthlyTimesheet.php | 85 ++-- .../Templates/showMyMonthlyTimesheet.tpl.php | 415 ++++++++---------- 2 files changed, 227 insertions(+), 273 deletions(-) diff --git a/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php b/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php index 44f7fda37f..e9952b9a42 100644 --- a/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php +++ b/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php @@ -50,51 +50,56 @@ public function init( * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function run(): Response - { - Auth::authOrRedirect([Roles::$owner, Roles::$admin, Roles::$manager, Roles::$editor], true); - - $fromDate = dtHelper()->userNow()->startOfMonth()->setToDbTimezone(); - - $kind = 'all'; - if (isset($_POST['search'])) { - // User date comes is in user date format and user timezone. Change it to utc. - if (! empty($_POST['startDate'])) { - try { - $fromDate = dtHelper()->parseUserDateTime($_POST['startDate'])->setToDbTimezone(); - } catch (\Exception $e) { - Log::warning($e); - Log::warning('User timezone: '.session('usersettings.timezone')); - Log::warning('User dateTime format: '.session('usersettings.date_format')); - $this->tpl->setNotification('Could not parse date', 'error', 'save_timesheet'); - } - } - } +public function run(): Response +{ + Auth::authOrRedirect([Roles::$owner, Roles::$admin, Roles::$manager, Roles::$editor], true); + + $fromDate = dtHelper()->userNow()->startOfMonth(); - if (isset($_POST['saveTimeSheet'])) { - $this->saveTimeSheet($_POST); + $kind = 'all'; + if (isset($_POST['search'])) { + // User date comes is in user date format and user timezone. Change it to utc. + if (! empty($_POST['startDate'])) { + try { + $fromDate = dtHelper()->parseUserDateTime($_POST['startDate'])->startOfMonth(); + + // Debug logging + error_log('Received startDate: ' . $_POST['startDate']); + error_log('Parsed to: ' . $fromDate->format('Y-m-d H:i:s')); + } catch (\Exception $e) { + error_log($e); + error_log('User timezone: '.session('usersettings.timezone')); + error_log('User dateTime format: '.session('usersettings.date_format')); + $this->tpl->setNotification('Could not parse date', 'error', 'save_timesheet'); + } } + } - $myTimesheets = $this->timesheetService->getWeeklyTimesheets(-1, $fromDate, session('userdata.id')); - $existingTicketIds = array_map(fn ($item) => $item['ticketId'], $myTimesheets); - - $this->tpl->assign('existingTicketIds', $existingTicketIds); - $this->tpl->assign('dateFrom', $fromDate); - $this->tpl->assign('actKind', $kind); - $this->tpl->assign('kind', $this->timesheetRepo->kind); - $this->tpl->assign('allProjects', $this->projects->getUserProjects( - userId: session('userdata.id'), - projectTypes: 'project' - )); - $this->tpl->assign('allTickets', $this->tickets->getUsersTickets( - id: session('userdata.id'), - limit: -1 - )); - $this->tpl->assign('allTimesheets', $myTimesheets); - - return $this->tpl->display('timesheets.showMyMonthlyTimesheet'); + if (isset($_POST['saveTimeSheet'])) { + $this->saveTimeSheet($_POST); } + $fromDateDb = $fromDate->copy()->setToDbTimezone(); + $myTimesheets = $this->timesheetService->getWeeklyTimesheets(-1, $fromDateDb, session('userdata.id')); + $existingTicketIds = array_map(fn ($item) => $item['ticketId'], $myTimesheets); + + $this->tpl->assign('existingTicketIds', $existingTicketIds); + $this->tpl->assign('dateFrom', $fromDate); + $this->tpl->assign('actKind', $kind); + $this->tpl->assign('kind', $this->timesheetRepo->kind); + $this->tpl->assign('allProjects', $this->projects->getUserProjects( + userId: session('userdata.id'), + projectTypes: 'project' + )); + $this->tpl->assign('allTickets', $this->tickets->getUsersTickets( + id: session('userdata.id'), + limit: -1 + )); + $this->tpl->assign('allTimesheets', $myTimesheets); + + return $this->tpl->display('timesheets.showMyMonthlyTimesheet'); +} + /** * @throws BindingResolutionException */ diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index 53fd2dcd0b..507cdaffbc 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -10,202 +10,195 @@ ?> @@ -226,7 +219,7 @@ ?> -
+
-
- - - __('label.until'); ?> - - - -
+
+ + + __('label.until'); ?> + + + +
@@ -267,43 +260,21 @@ __('language.dayNamesShort')); - // Make the first day of week monday, by shifting sunday to the back of the array. - $days[] = array_shift($days); + $dateFrom = $tpl->get('dateFrom'); + $daysInMonth = $dateFrom->daysInMonth(); + for ($d = 1; $d <= $daysInMonth; $d++) { + echo "{$d}"; + } ?> __('label.client_product') ?> __('subtitles.todo') ?> __('label.type') ?> - -
- addDays($i)->formatDateForUser(); - $i++; - ?> - - __('label.total') ?> 0, - 'day2' => 0, - 'day3' => 0, - 'day4' => 0, - 'day5' => 0, - 'day6' => 0, - 'day7' => 0, - ]; // @todo: move all this calculations into the service the timesheets class. foreach ($tpl->get('allTimesheets') as $timeRow) { $timesheetId = 'new'; @@ -423,34 +394,12 @@ class="hourCell" - - - - - - __('label.total') ?> - $col) { - $totalHours += $col; - - ?> - - - From dce0a3c61e66cc1ee579f6e53952cbc028bb8c21 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 7 Jan 2026 11:36:39 +0100 Subject: [PATCH 07/28] #5418 Add displaying of all days in the month --- .../Templates/showMyMonthlyTimesheet.tpl.php | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index 507cdaffbc..bb6d4969bd 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -259,6 +259,10 @@ function buildMonthTable(date) { + + __('label.client_product') ?> + __('subtitles.todo') ?> + __('label.type') ?> get('dateFrom'); $daysInMonth = $dateFrom->daysInMonth(); @@ -266,10 +270,6 @@ function buildMonthTable(date) { echo "{$d}"; } ?> - - __('label.client_product') ?> - __('subtitles.todo') ?> - __('label.type') ?> __('label.total') ?> @@ -294,44 +294,45 @@ function buildMonthTable(date) { - - - - - - formatDateForUser() : 'false') . '|' . ($timeRow[$dayKey]['actualWorkDate'] ? $timeRow[$dayKey]['actualWorkDate']->getTimestamp() : 'false'); - ?> - - name="" - value="" - data-decimal-value="" - - data-tippy-content="Cannot add time entry in previous timezone" - /> - - - - - - - - - - + + + formatDateForUser() . '|' . $dayData['actualWorkDate']->getTimestamp(); + ?> + + + + + + + + + + + + + + + + + From 9b06548463873212b1719334998a293e1e6c8412 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 7 Jan 2026 13:10:12 +0100 Subject: [PATCH 08/28] #5418 Add automatic resizing to the table cols --- .../Templates/showMyMonthlyTimesheet.tpl.php | 309 +++++++++--------- 1 file changed, 156 insertions(+), 153 deletions(-) diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index bb6d4969bd..b6c0419984 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -10,107 +10,107 @@ ?> @@ -236,29 +185,13 @@ function buildMonthTable(date) {
- - - - - - - - - - - - - - - +
get('dateFrom'); - $daysInMonth = $dateFrom->daysInMonth(); for ($d = 1; $d <= $daysInMonth; $d++) { echo ""; } @@ -270,7 +203,6 @@ function buildMonthTable(date) { get('allTimesheets') as $timeRow) { - $timesheetId = 'new'; ?> @@ -306,8 +238,8 @@ function buildMonthTable(date) { ?> " + style="width: 70px;" + name=" " value="" data-decimal-value="" /> @@ -329,7 +261,6 @@ class="hourCell" - @@ -419,13 +350,13 @@ class="hourCell" $col = $colSum[$dayKey] ?? 0; $totalHours += $col; ?> - - +
__('label.client_product') ?> __('subtitles.todo') ?> __('label.type') ?> {$d}
e($timeRow['clientName']); ?> // e($timeRow['name']); ?>
+ 0) { echo format_hours($col); } ?>
From f11e991eebf907e64df1c51e06df9321dec074a8 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 7 Jan 2026 16:31:44 +0100 Subject: [PATCH 11/28] #5418 Hide css classes that caused UI bug, add autosave on month choose --- .../Controllers/ShowMyMonthlyTimesheet.php | 39 +----------- .../Templates/showMyMonthlyTimesheet.tpl.php | 59 ++++++++++++------- 2 files changed, 41 insertions(+), 57 deletions(-) diff --git a/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php b/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php index e9952b9a42..b80a82c665 100644 --- a/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php +++ b/app/Domain/Timesheets/Controllers/ShowMyMonthlyTimesheet.php @@ -2,9 +2,6 @@ namespace Leantime\Domain\Timesheets\Controllers; -use Carbon\CarbonInterface; -use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Support\Facades\Log; use Leantime\Core\Controller\Controller; use Leantime\Domain\Auth\Models\Roles; use Leantime\Domain\Auth\Services\Auth; @@ -12,7 +9,6 @@ use Leantime\Domain\Tickets\Repositories\Tickets as TicketRepository; use Leantime\Domain\Timesheets\Repositories\Timesheets as TimesheetRepository; use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService; -use Leantime\Domain\Users\Repositories\Users as UserRepository; use Symfony\Component\HttpFoundation\Response; class ShowMyMonthlyTimesheet extends Controller @@ -25,31 +21,18 @@ class ShowMyMonthlyTimesheet extends Controller private TicketRepository $tickets; - private UserRepository $userRepo; - - /** - * init - initialze private variables - */ public function init( TimesheetService $timesheetService, TimesheetRepository $timesheetRepo, ProjectRepository $projects, TicketRepository $tickets, - UserRepository $userRepo ): void { $this->timesheetService = $timesheetService; $this->timesheetRepo = $timesheetRepo; $this->projects = $projects; $this->tickets = $tickets; - $this->userRepo = $userRepo; } - /** - * run - display template and edit data - * - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ public function run(): Response { Auth::authOrRedirect([Roles::$owner, Roles::$admin, Roles::$manager, Roles::$editor], true); @@ -58,18 +41,10 @@ public function run(): Response $kind = 'all'; if (isset($_POST['search'])) { - // User date comes is in user date format and user timezone. Change it to utc. if (! empty($_POST['startDate'])) { try { $fromDate = dtHelper()->parseUserDateTime($_POST['startDate'])->startOfMonth(); - - // Debug logging - error_log('Received startDate: ' . $_POST['startDate']); - error_log('Parsed to: ' . $fromDate->format('Y-m-d H:i:s')); } catch (\Exception $e) { - error_log($e); - error_log('User timezone: '.session('usersettings.timezone')); - error_log('User dateTime format: '.session('usersettings.date_format')); $this->tpl->setNotification('Could not parse date', 'error', 'save_timesheet'); } } @@ -100,14 +75,10 @@ public function run(): Response return $this->tpl->display('timesheets.showMyMonthlyTimesheet'); } - /** - * @throws BindingResolutionException - */ public function saveTimeSheet(array $postData): void { foreach ($postData as $key => $dateEntry) { - // The temp data should contain four parts, spectated by "|": - // TICKET ID | Type of booked hours | Date | Timestamp + $tempData = explode('|', $key); if (count($tempData) === 4) { @@ -117,7 +88,6 @@ public function saveTimeSheet(array $postData): void $timestamp = $tempData[3]; $hours = $dateEntry; - // if ticket ID is set to new, pull id and hour type from form field if ($ticketId === 'new' || $ticketId === 0) { $ticketId = (int) $postData['ticketId']; $kind = $postData['kindId']; @@ -129,21 +99,18 @@ public function saveTimeSheet(array $postData): void } } - // Parse hours using TimeParser for flexible input formats $parsedHours = $hours; if (!empty($hours) && !is_numeric($hours)) { try { $parser = app(\Leantime\Domain\Timesheets\Services\TimeParser::class); $parsedHours = $parser->parseTimeToDecimal($hours); - // Additional validation: check for unreasonably large values - if ($parsedHours > 24 * 365) { // More than a year of work + if ($parsedHours > 24 * 365) { throw new \InvalidArgumentException('Time value is unreasonably large. Please enter a valid amount of time.'); } } catch (\InvalidArgumentException $e) { $this->tpl->setNotification($e->getMessage(), 'error', 'time_parse_error'); - // Skip saving this entry if parsing failed continue; } } @@ -157,7 +124,6 @@ public function saveTimeSheet(array $postData): void 'kind' => $kind, ]; - // This should not be the case since we set the input to disabled, but check anyways if ($timestamp !== 'false' && $timestamp != false) { try { $this->timesheetService->upsertTime($ticketId, $values); @@ -165,7 +131,6 @@ public function saveTimeSheet(array $postData): void } catch (\Exception $e) { $this->tpl->setNotification('Error logging time: '.$e->getMessage(), 'error', 'save_timesheet'); report($e); - continue; } } diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index cf87cb4e01..2fe27608b2 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -1,10 +1,9 @@ $val) { - $$var = $val; // necessary for blade refactor + $$var = $val; } -/** @var \Carbon\Carbon $currentDate */ $dateFrom = $tpl->get('dateFrom'); $daysInMonth = $dateFrom->daysInMonth(); $hoursFormat = session('usersettings.hours_format', 'decimal'); @@ -29,14 +28,32 @@ selectOtherMonths: true, changeMonth: true, changeYear: true, - showButtonPanel: true, - onSelect: function(dateText, inst) { + onClose: function(dateText, inst) { + var month = jQuery("#ui-datepicker-div .ui-datepicker-month :selected").val(); + var year = jQuery("#ui-datepicker-div .ui-datepicker-year :selected").val(); + jQuery(this).data('datepicker-open', false); + jQuery(this).datepicker('setDate', new Date(year, month, 1)); jQuery(this).change(); jQuery("#timesheetList").submit(); - } + }, + beforeShow: function(input, inst) { + jQuery("#ui-datepicker-div").addClass('hide-calendar'); + jQuery(this).data('datepicker-open', true); + }, + onChangeMonthYear: function(year, month, inst) { + if (jQuery(this).data('datepicker-open')) { + var startDateStr = year + '-' + String(month).padStart(2, '0') + '-01'; + var lastDay = new Date(year, month, 0); + var endDateStr = year + '-' + String(month).padStart(2, '0') + '-' + String(lastDay.getDate()).padStart(2, '0'); + + jQuery('#startDate').val(startDateStr); + jQuery('#endDate').val(endDateStr); + + jQuery("#timesheetList").submit(); + } + }, }); - // Set initial dates from server if (initStartDate) { jQuery('#startDate').datepicker('setDate', initStartDate); } @@ -84,18 +101,14 @@ jQuery("#timesheetList").submit(); }); - // Calculate sums on input change - // Calculate sums on input change jQuery(".timesheetTable input.hourCell").change(function() { var daysInMonth = parseInt(jQuery(".timesheetTable").data('days-in-month')); - // Initialize column sums let colSums = {}; for (let d = 1; d <= daysInMonth; d++) { colSums['day' + d] = 0; } - // Calculate row and column sums jQuery(".timesheetRow").each(function(i) { var rowSum = 0; @@ -117,7 +130,6 @@ jQuery(this).find(".rowSum strong").text(format_hours(rowSum)); }); - // Update column totals var finalSum = 0; for (let d = 1; d <= daysInMonth; d++) { var dayKey = 'day' + d; @@ -137,14 +149,25 @@ jQuery("#finalSum").attr('data-decimal', roundedSum); }); - // Optional: if user is manager, init edit modal leantime.timesheetsController.initEditTimeModal(); }); + - -
@@ -176,9 +198,9 @@
- + __('label.until'); ?> - +
@@ -201,7 +223,6 @@ get('allTimesheets') as $timeRow) { ?> @@ -220,7 +241,6 @@ -
@@ -324,7 +343,7 @@ class="hourCell" copy()->setDay($d); // Use copy() to avoid modifying the original + $dayDate = $dateFrom->copy()->setDay($d); ?> copy()->setDay($d); + $dayDate = $dateFrom->copy()->setDay($d); ?>
  • __('links.week_view') ?>
  • __('links.list_view') ?>
  • -
  • Monthly View
  • +
  • Monthly View
  • diff --git a/app/Domain/Timesheets/Templates/showMyList.tpl.php b/app/Domain/Timesheets/Templates/showMyList.tpl.php index b23e019bce..80d9c0c0cc 100644 --- a/app/Domain/Timesheets/Templates/showMyList.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyList.tpl.php @@ -75,7 +75,7 @@ class="dateTo"
    diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index daa7d1d1a0..8c779251ab 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -186,12 +186,12 @@
    From a3550c8b18e871f1fef45dc61e1c388e91eee719 Mon Sep 17 00:00:00 2001 From: Haris Macic Date: Wed, 7 Jan 2026 17:22:32 +0100 Subject: [PATCH 14/28] #5418 Adjust layout of month and year calendar --- .../Templates/showMyMonthlyTimesheet.tpl.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php index 8c779251ab..648364cc5e 100644 --- a/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyMonthlyTimesheet.tpl.php @@ -166,6 +166,21 @@ .ui-datepicker-next { display: none !important; } + + .ui-datepicker-header.ui-widget-header.ui-helper-clearfix.ui-corner-all { + background: white !important; + max-height: 40px; + } + + .ui-datepicker-month { + margin-right: 20px; + } + + .ui-datepicker-title { + display: flex; + align-items: center; + justify-content: center; + }