diff --git a/.idea/php.xml b/.idea/php.xml index 1f9ad773bc..c4bca77530 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -1,5 +1,25 @@ + + + + + + + + + + + + + + + + @@ -288,4 +308,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/Domain/Tickets/Services/Tickets.php b/app/Domain/Tickets/Services/Tickets.php index d06b32a4ee..9b17443576 100644 --- a/app/Domain/Tickets/Services/Tickets.php +++ b/app/Domain/Tickets/Services/Tickets.php @@ -1514,7 +1514,8 @@ public function quickAddTicket($params): array|bool $notification->authorId = session('userdata.id') ?? -1; $notification->message = $message; - $this->projectService->notifyProjectUsers($notification); + // Disabled: Don't notify project users when a ticket is created + // $this->projectService->notifyProjectUsers($notification); return $result; } @@ -1673,7 +1674,8 @@ public function addTicket($values): array|int|bool $notification->authorId = session('userdata.id') ?? -1; $notification->message = $message; - $this->projectService->notifyProjectUsers($notification); + // Disabled: Don't notify project users when a ticket is created + // $this->projectService->notifyProjectUsers($notification); return $addTicketResponse; } @@ -2954,10 +2956,11 @@ public function prepareTicketDates(&$values) )->formatDateTimeForDb(); unset($values['timeFrom']); } else { + // Use noon (12:00:00) instead of start of day to preserve date when converting to UTC $values['editFrom'] = dtHelper()->parseUserDateTime( $values['editFrom'], 'start' - )->formatDateTimeForDb(); + )->setTime(12, 0, 0)->formatDateTimeForDb(); } } } catch (\Exception $e) { @@ -2979,10 +2982,11 @@ public function prepareTicketDates(&$values) )->formatDateTimeForDb(); unset($values['timeTo']); } else { + // Use noon (12:00:00) instead of end of day to preserve date when converting to UTC $values['editTo'] = dtHelper()->parseUserDateTime( $values['editTo'], - 'end' - )->formatDateTimeForDb(); + 'start' + )->setTime(12, 0, 0)->formatDateTimeForDb(); } } diff --git a/app/Domain/Tickets/Templates/partials/ticketsubmenu.blade.php b/app/Domain/Tickets/Templates/partials/ticketsubmenu.blade.php index cc15be36b1..d88bcc4fa3 100644 --- a/app/Domain/Tickets/Templates/partials/ticketsubmenu.blade.php +++ b/app/Domain/Tickets/Templates/partials/ticketsubmenu.blade.php @@ -5,7 +5,6 @@ ]) @if ($login::userIsAtLeast(\Leantime\Domain\Auth\Models\Roles::$editor)) -
@@ -27,6 +26,23 @@ class="add-subtask-link"> Add Subtask @endif +
  • + + + + + + + + + + + + + + + +
  • @dispatchEvent("beforeDeleteTicket", ["ticket"=>$ticket])
  • {{ __("links.delete_todo") }}
  • @@ -43,3 +59,20 @@ class="add-subtask-link">
    @endif + \ No newline at end of file diff --git a/app/Domain/Tickets/Templates/showTicketModal.blade.php b/app/Domain/Tickets/Templates/showTicketModal.blade.php index 71fc4ffc6d..7b3b90001d 100644 --- a/app/Domain/Tickets/Templates/showTicketModal.blade.php +++ b/app/Domain/Tickets/Templates/showTicketModal.blade.php @@ -44,7 +44,6 @@
  • __('links.move_todo'); ?>
  • __('links.delete_todo'); ?>
  • -
  • @@ -62,6 +61,7 @@
  • +
  • - - +
    + + @@ -79,7 +80,7 @@ - + __(strtolower($kindMap[$entry['kind']])); } - $hoursFormatted = format($entry['hours'])->decimal(); + $hoursFormatted = format_hours($entry['hours']); $description = $entry['description'] ?? ''; + $entryId = $entry['id'] ?? ''; ?> + @@ -148,6 +159,31 @@ leantime.ticketsController.initTimeSheetChart(labels, d2, d3, "canvas") + // Initialize editTimeModal for timesheet entries + if (typeof leantime.timesheetsController !== 'undefined' && typeof leantime.timesheetsController.initEditTimeModal === 'function') { + leantime.timesheetsController.initEditTimeModal(); + } else { + // Fallback: Initialize nyroModal directly if timesheetsController is not available + var canvasoptions = { + sizes: { + minW: 700, + minH: 1000, + }, + resizable: true, + autoSizable: true, + callbacks: { + beforeShowCont: function () { + jQuery(".showDialogOnLoad").show(); + }, + beforeClose: function () { + location.reload(); + } + }, + titleFromIframe: true + }; + jQuery(".editTimeModal").nyroModal(canvasoptions); + } + }); diff --git a/app/Domain/Timesheets/Controllers/EditTime.php b/app/Domain/Timesheets/Controllers/EditTime.php index b37b1783e4..a5f729aa02 100644 --- a/app/Domain/Timesheets/Controllers/EditTime.php +++ b/app/Domain/Timesheets/Controllers/EditTime.php @@ -215,7 +215,7 @@ public function run(): Response $this->tpl->assign('allTickets', $this->tickets->getAll()); $this->tpl->assign('kind', $this->timesheetsRepo->kind); - return $this->tpl->display('timesheets.editTime'); + return $this->tpl->displayPartial('timesheets.editTime'); } else { return $this->tpl->displayPartial('errors.error403'); } diff --git a/app/Domain/Timesheets/Controllers/ShowAll.php b/app/Domain/Timesheets/Controllers/ShowAll.php index 4d91ea89c6..9b08f7edce 100644 --- a/app/Domain/Timesheets/Controllers/ShowAll.php +++ b/app/Domain/Timesheets/Controllers/ShowAll.php @@ -212,6 +212,15 @@ public function run(): Response $clientId )); + // Pass user's hours format preference to template for CSV export + $userId = session('userdata.id'); + $hoursFormat = 'decimal'; + if ($userId) { + $settingsService = app()->make(\Leantime\Domain\Setting\Services\Setting::class); + $hoursFormat = $settingsService->getSetting('usersettings.'.$userId.'.hours_format', 'decimal'); + } + $this->tpl->assign('hoursFormat', $hoursFormat); + return $this->tpl->display('timesheets.showAll'); } } diff --git a/app/Domain/Timesheets/Controllers/ShowMyList.php b/app/Domain/Timesheets/Controllers/ShowMyList.php index c5e2b8cc23..a882c7f8fc 100644 --- a/app/Domain/Timesheets/Controllers/ShowMyList.php +++ b/app/Domain/Timesheets/Controllers/ShowMyList.php @@ -64,6 +64,15 @@ public function run(): Response paid: -1 )); + // Pass user's hours format preference to template for CSV export + $userId = session('userdata.id'); + $hoursFormat = 'decimal'; + if ($userId) { + $settingsService = app()->make(\Leantime\Domain\Setting\Services\Setting::class); + $hoursFormat = $settingsService->getSetting('usersettings.'.$userId.'.hours_format', 'decimal'); + } + $this->tpl->assign('hoursFormat', $hoursFormat); + return $this->tpl->display('timesheets.showMyList'); } } diff --git a/app/Domain/Timesheets/Js/timesheetsExportFormatter.js b/app/Domain/Timesheets/Js/timesheetsExportFormatter.js new file mode 100644 index 0000000000..0d0737142f --- /dev/null +++ b/app/Domain/Timesheets/Js/timesheetsExportFormatter.js @@ -0,0 +1,40 @@ +/** + * Timesheets export helper + * + * Provides a single place to resolve CSV export values while + * keeping the original markup untouched (aside from data attributes). + */ +(function () { + window.leantime = window.leantime || {}; + window.leantime.timesheetsExport = window.leantime.timesheetsExport || {}; + + /** + * Resolve the value that should be exported for a cell. + * + * @param {jQuery} $node + * @param {string} fallbackData + * @returns {string} + */ + window.leantime.timesheetsExport.resolveCell = function ($node, fallbackData) { + if (typeof $node.data('order') === 'undefined') { + return fallbackData; + } + + if (! $node.hasClass('js-timesheet-hours')) { + return $node.data('order'); + } + + var tableFormat = ($node.closest('table[data-hours-format]').data('hours-format') || '').toString(); + + if (tableFormat === 'human') { + if (typeof $node.data('export-display') !== 'undefined') { + return $node.data('export-display'); + } + + return $node.text().trim(); + } + + return $node.data('order'); + }; +})(); + diff --git a/app/Domain/Timesheets/Repositories/Timesheets.php b/app/Domain/Timesheets/Repositories/Timesheets.php index 187b978afb..bd71f7e4dd 100644 --- a/app/Domain/Timesheets/Repositories/Timesheets.php +++ b/app/Domain/Timesheets/Repositories/Timesheets.php @@ -665,7 +665,7 @@ public function addTime(array $values): void $call = $this->dbcall(func_get_args()); $call->prepare($query); - + $workDateTime = new \DateTime($values['date']); $currentTime = new \DateTime('now'); @@ -676,7 +676,7 @@ public function addTime(array $values): void $workDateTime->setTime($currentHours, $currentMinutes, $currentSeconds); $combinedWorkDate = $workDateTime->format('Y-m-d H:i:s'); - + $call->bindValue(':userId', $values['userId']); $call->bindValue(':ticket', $values['ticket']); $call->bindValue(':loggingDate', $combinedWorkDate); @@ -775,7 +775,7 @@ public function punchOut(int $ticketId): float|false|int $call->bindValue(':workDate', $formattedDate); - + $query = "INSERT INTO `zp_timesheets` (userId, ticketId, workDate, hours, kind, modified) VALUES (:sessionId, :ticketId, :workDate, :hoursWorked, 'GENERAL_BILLABLE', :modified) ON DUPLICATE KEY UPDATE hours = hours + :hoursWorked"; @@ -864,6 +864,29 @@ public function upsertTimesheetEntry(array $values): void */ public function updateTime(array $values): void { + // Get the original timesheet to preserve the time component + $originalTimesheet = $this->getTimesheet($values['id']); + + // If we have an original timesheet, preserve its time component + if ($originalTimesheet && !empty($originalTimesheet['workDate'])) { + $originalDate = new Carbon($originalTimesheet['workDate'], 'UTC'); + + if (is_string($values['date'])) { + $newDate = new Carbon($values['date'], 'UTC'); + } else { + $newDate = $values['date']; + } + + // Preserve original time (hour, minute, second) with new date + $preservedDateTime = $newDate->setTime( + $originalDate->hour, + $originalDate->minute, + $originalDate->second + ); + + $values['date'] = $preservedDateTime->format('Y-m-d H:i:s'); + } + $query = 'UPDATE zp_timesheets SET diff --git a/app/Domain/Timesheets/Templates/editTime.tpl.php b/app/Domain/Timesheets/Templates/editTime.tpl.php index e9f68a777c..dcc61059d4 100644 --- a/app/Domain/Timesheets/Templates/editTime.tpl.php +++ b/app/Domain/Timesheets/Templates/editTime.tpl.php @@ -54,11 +54,10 @@ displayNotification() ?> - -

    __('headlines.edit_todo'); ?>

    +

    __('headlines.edit_todo'); ?>

    - +
    - +
    - get('kind') as $key => $row) { @@ -100,13 +99,13 @@ ?>
    -__('label.date')?>
    -__('label.hours')?>
    -
    @@ -119,23 +118,23 @@ echo ' checked="checked"'; } ?> /> - + - __('label.date') ?> __('label.date') ?>


    - /> - - __('label.date') ?> __('label.invoiced_comp') ?> + __('label.date') ?>
    /> - - __('label.date') ?> __('label.paid') ?> + __('label.date') ?>

    - __('links.delete') ?> + __('links.delete') ?>

    - - - - - + \ No newline at end of file diff --git a/app/Domain/Timesheets/Templates/showAll.tpl.php b/app/Domain/Timesheets/Templates/showAll.tpl.php index 677a316f70..2484ad936d 100644 --- a/app/Domain/Timesheets/Templates/showAll.tpl.php +++ b/app/Domain/Timesheets/Templates/showAll.tpl.php @@ -4,8 +4,38 @@ foreach ($__data as $var => $val) { $$var = $val; // necessary for blade refactor } +$hoursFormat = session('usersettings.hours_format', 'decimal'); ?> @@ -342,7 +345,7 @@ class="hourCell" /> - + diff --git a/app/Domain/Timesheets/Templates/showMyList.tpl.php b/app/Domain/Timesheets/Templates/showMyList.tpl.php index 20ac115dcd..4791ab2ef9 100644 --- a/app/Domain/Timesheets/Templates/showMyList.tpl.php +++ b/app/Domain/Timesheets/Templates/showMyList.tpl.php @@ -6,6 +6,7 @@ foreach ($__data as $var => $val) { $$var = $val; // necessary for blade refactor } +$hoursFormat = session('usersettings.hours_format', 'decimal'); ?> @@ -84,7 +85,7 @@ class="dateTo"
    -
    __('label.id'); ?> __('label.date'); ?> __('label.user'); ?> __('label.type'); ?>__('label.description'); ?>
    + + + # + + + - + +
    +
    @@ -133,15 +134,18 @@ class="dateTo" date(); ?> time(); ?> - */ ?> + - */ ?> + - */ ?> + - + */ ?> +
    - e($row['hours'] ?: 0); ?> + e($row['hours']); ?>">e($row['hours'] ?: 0); ?> + - e($row['planHours'] ?: 0); ?> + e($row['planHours']); ?>">e($row['planHours'] ?: 0); ?> + - + "> + e($row['headline']); ?> @@ -193,7 +197,8 @@ class="dateTo"
    __('label.total_hours')?>
    @@ -202,6 +207,35 @@ class="dateTo"