Skip to content
This repository was archived by the owner on Oct 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .idea/php.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions app/Domain/Tickets/Services/Tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}
}

Expand Down
35 changes: 34 additions & 1 deletion app/Domain/Tickets/Templates/partials/ticketsubmenu.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
])

@if ($login::userIsAtLeast(\Leantime\Domain\Auth\Models\Roles::$editor))

<div class="inlineDropDownContainer" style="float:right;">

<a href="javascript:void(0);" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
Expand All @@ -27,6 +26,23 @@
class="add-subtask-link">
<i class="fa-solid fa-diagram-predecessor"></i> Add Subtask</a></li>
@endif
<li><form method="POST" action="<?= BASE_URL ?>/tickets/cloneTicket" >
<input type="hidden" name="id" value="<?= $ticket['id'] ?>">
<input type="hidden" name="description" value="<?= htmlspecialchars($ticket['description']) ?>">
<input type="hidden" name="projectId" value="<?= $ticket['projectId'] ?>">
<input type="hidden" name="status" value="<?= $ticket['status'] ?>">
<input type="hidden" name="priority" value="<?= $ticket['priority'] ?>">
<input type="hidden" name="effort" value="<?= $ticket['storypoints'] ?>">
<input type="hidden" name="dueDate" value="<?= $ticket['dateToFinish'] ?>">
<input type="hidden" name="tags" value="<?= $ticket['tags'] ?>">
<input type="hidden" name="milestone" value="<?= $ticket['milestoneid'] ?>">
<input type="hidden" name="relatedTo" value="<?= $ticket['dependingTicketId'] ?>">
<input type="hidden" name="workStart" value="<?= $ticket['editFrom'] ?>">
<input type="hidden" name="workEnd" value="<?= $ticket['editTo'] ?>">
<input type="hidden" name="planHours" value="<?= $ticket['planHours'] ?>">
<input type="hidden" name="hourRemaining" value="<?= $ticket['hourRemaining'] ?>">
<button type="submit" class="submit_button"><i class="fa fa-clone"></i> Clone To-Do</button>
</form></li>
@dispatchEvent("beforeDeleteTicket", ["ticket"=>$ticket])
<li><a href="#/tickets/delTicket/{{ $ticket["id"] }}" class="delete"><i class="fa fa-trash"></i> {{ __("links.delete_todo") }}</a></li>

Expand All @@ -43,3 +59,20 @@ class="add-subtask-link">
</div>

@endif
<style>
.submit_button {
background-color: white;
color: black;
border: none;
padding-left: 10px;
width: 100%;
text-align: left;
height: 30px;
}
.submit_button:hover {
background-color: #f0f0f0;
cursor: pointer;
color: black;
}

</style>
2 changes: 1 addition & 1 deletion app/Domain/Tickets/Templates/showTicketModal.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
<li class="nav-header"><?php echo $tpl->__('subtitles.todo'); ?></li>
<li><a href="#/tickets/moveTicket/<?php echo $ticket->id; ?>" class="moveTicketModal sprintModal ticketModal"><i class="fa-solid fa-arrow-right-arrow-left"></i> <?php echo $tpl->__('links.move_todo'); ?></a></li>
<li><a href="#/tickets/delTicket/<?php echo $ticket->id; ?>" class="delete"><i class="fa fa-trash"></i> <?php echo $tpl->__('links.delete_todo'); ?></a></li>
<li class="nav-header border"><?php echo $tpl->__('subtitles.track_time'); ?></li>
<li><form method="POST" action="<?= BASE_URL ?>/tickets/cloneTicket">
<input type="hidden" name="id" value="<?= $ticket->id ?>">
<input type="hidden" name="description" value="<?= htmlspecialchars($ticket->description) ?>">
Expand All @@ -62,6 +61,7 @@
<input type="hidden" name="hourRemaining" value="<?= $ticket->hourRemaining?>">
<button type="submit" class="button-primary"><i class="fa fa-clone"></i> Clone To-Do</button>
</form></li>
<li class="nav-header border"><?php echo $tpl->__('subtitles.track_time'); ?></li>
<li id="timerContainer-ticketDetails-{{ $ticket->id }}"
hx-get="{{BASE_URL}}/tickets/timerButton/get-status/{{ $ticket->id }}"
hx-trigger="timerUpdate from:body"
Expand Down
44 changes: 40 additions & 4 deletions app/Domain/Tickets/Templates/submodules/timesheet.sub.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,18 @@

<?php if (! empty($timesheetEntries)) { ?>
<div class="table-responsive">
<table class="table table-bordered" style="background-color: #fff; border-collapse: collapse;">
<thead style="background-color: #f9f9f9;">
<table class="table table-bordered" style="border-collapse: collapse;">
<thead>
<tr>
<th style="font-weight: bold; padding: 12px; border: 1px solid #e0e0e0;"><?php echo $tpl->__('label.id'); ?></th>
<th style="font-weight: bold; padding: 12px; border: 1px solid #e0e0e0;"><?php echo $tpl->__('label.date'); ?></th>
<th style="font-weight: bold; padding: 12px; border: 1px solid #e0e0e0;"><?php echo $tpl->__('label.user'); ?></th>
<th style="font-weight: bold; padding: 12px; border: 1px solid #e0e0e0;"><?php echo $tpl->__('label.type'); ?></th>
<th style="font-weight: bold; padding: 12px; border: 1px solid #e0e0e0;"><?php echo $tpl->__('label.hours'); ?></th>
<th style="font-weight: bold; padding: 12px; border: 1px solid #e0e0e0;"><?php echo $tpl->__('label.description'); ?></th>
</tr>
</thead>
<tbody style="background-color: #fff;">
<tbody>
<?php foreach ($timesheetEntries as $entry) {
$date = new \DateTime($entry['workDate']);

Expand All @@ -99,10 +100,20 @@
$kindLabel = $tpl->__(strtolower($kindMap[$entry['kind']]));
}

$hoursFormatted = format($entry['hours'])->decimal();
$hoursFormatted = format_hours($entry['hours']);
$description = $entry['description'] ?? '';
$entryId = $entry['id'] ?? '';
?>
<tr>
<td style="padding: 10px; border: 1px solid #e0e0e0;">
<?php if (!empty($entryId)) { ?>
<a href="<?= BASE_URL ?>/timesheets/editTime/<?= htmlspecialchars($entryId, ENT_QUOTES, 'UTF-8') ?>" class="editTimeModal" id="editTimesheet-<?= htmlspecialchars($entryId, ENT_QUOTES, 'UTF-8') ?>">
#<?= htmlspecialchars($entryId, ENT_QUOTES, 'UTF-8') ?>
</a>
<?php } else { ?>
-
<?php } ?>
</td>
<td style="padding: 10px; border: 1px solid #e0e0e0;"><?php echo htmlspecialchars($displayDate, ENT_QUOTES, 'UTF-8'); ?></td>
<td style="padding: 10px; border: 1px solid #e0e0e0;"><?php echo htmlspecialchars($userName, ENT_QUOTES, 'UTF-8'); ?></td>
<td style="padding: 10px; border: 1px solid #e0e0e0;"><?php echo htmlspecialchars($kindLabel, ENT_QUOTES, 'UTF-8'); ?></td>
Expand Down Expand Up @@ -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);
}

});

</script>
2 changes: 1 addition & 1 deletion app/Domain/Timesheets/Controllers/EditTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
9 changes: 9 additions & 0 deletions app/Domain/Timesheets/Controllers/ShowAll.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
9 changes: 9 additions & 0 deletions app/Domain/Timesheets/Controllers/ShowMyList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
40 changes: 40 additions & 0 deletions app/Domain/Timesheets/Js/timesheetsExportFormatter.js
Original file line number Diff line number Diff line change
@@ -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');
};
})();

29 changes: 26 additions & 3 deletions app/Domain/Timesheets/Repositories/Timesheets.php
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@
$call = $this->dbcall(func_get_args());

$call->prepare($query);

$workDateTime = new \DateTime($values['date']);
$currentTime = new \DateTime('now');

Expand All @@ -676,7 +676,7 @@
$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);
Expand Down Expand Up @@ -774,8 +774,8 @@
$formattedDate = $workDate->setTimezone('CEST')->format('Y-m-d H:i:s');


$call->bindValue(':workDate', $formattedDate);

Check failure on line 777 in app/Domain/Timesheets/Repositories/Timesheets.php

View workflow job for this annotation

GitHub Actions / phpstan

Undefined variable: $call

$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";
Expand Down Expand Up @@ -864,6 +864,29 @@
*/
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
Expand Down
Loading
Loading