Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ee3c20c
[#6002] Add listener for nyroModal and wait it to be initialized
harismacic1 Feb 23, 2026
c4f544b
#6003 Save old filters after navigating in company page
harismacic1 Feb 23, 2026
091f676
#6003 Add listener for routes on which user navigates, so filters are…
harismacic1 Feb 24, 2026
8660b11
#5984 Add global css font colors
harismacic1 Feb 24, 2026
f4588b3
#5984 Add appropriate colors in leantime scheme
harismacic1 Feb 24, 2026
4cd39cc
#5984 Change font color on dark theme default to white
harismacic1 Feb 25, 2026
dcda059
#5984 Remove custom css from gitignore
harismacic1 Feb 25, 2026
82a08d7
#5519 Add initial structure of repository, template, controller and s…
harismacic1 Feb 17, 2026
2bf788f
#5519 Fix controller, enable saving templates to database
harismacic1 Feb 17, 2026
e275448
#5519 Add logic for selecting templates in ticket description
harismacic1 Feb 17, 2026
1751170
[#5519] Remove comments and unused component
harismacic1 Feb 17, 2026
a7fe48a
#5519 Replace built in dialog pop up with custom modal
harismacic1 Feb 18, 2026
8eebb4b
#6075 Change leantime font color
harismacic1 Feb 25, 2026
b8c0dda
#6073 Add logic for parsing all date formats
harismacic1 Feb 25, 2026
26aaa50
#6015 Fix automated csv export on profiles
farukvukovic Feb 20, 2026
6a72467
#6015 - Removed 2 second pause beetween sending of slack exports
farukvukovic Feb 25, 2026
bcef183
#6015 Add upload rate exceeded error notification
farukvukovic Feb 25, 2026
f3be985
#6078 Add logic for milestone filters in ShowAll controller, Timeshee…
harismacic1 Feb 26, 2026
00e0ba1
#6078 Add overflow to the whole table
harismacic1 Feb 26, 2026
9ff3e50
#6084 Add missing jquery to timesheetsController to prevent default b…
harismacic1 Feb 26, 2026
7b21bca
Add Clear All to notifications
farukvukovic Feb 26, 2026
b1e7d03
[#5519] Make whole div clickable and put cursor pointer on it
harismacic1 Feb 26, 2026
7c444a1
#6003 Resolve UI issues with date picker in All timesheets
harismacic1 Feb 27, 2026
3aae03c
#6112 Fix Export to slack hours in human readible form
farukvukovic Feb 27, 2026
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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
trim_trailing_whitespace = false
indent_style = space

# 4 space indenting
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions app/Domain/Menu/Templates/headMenu.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ class="dropdown-toggle profileHandler notificationHandler"

<div class="scroll-wrapper">

<div style="display: flex; justify-content: flex-end; padding: 5px 10px;">
<a href="javascript:void(0);" id="clearAllNotifications" style="font-size: 12px; color: var(--primary-font-color)">
{{ __('text.clear_all') ?? 'Clear all' }}
</a>
</div>

<ul id='notificationsList' class='notificationViewLists'>
@if ($totalNotificationCount === 0)
<p style='padding: 10px'>{{ __('text.no_notifications') }}</p>
Expand Down Expand Up @@ -310,6 +316,25 @@ class="active"

window.location.href = url;
})

jQuery('#clearAllNotifications').on('click', function (e) {
e.stopPropagation();

jQuery.ajax({
type: 'PATCH',
url: leantime.appUrl + '/api/notifications',
data: {
id: 'all',
action: 'read'
}
}).done(function () {
jQuery("#notificationsDropdown .notificationViewLists li.new").removeClass("new");
jQuery("#notificationsDropdown .notificationCounter").fadeOut();
jQuery('.notificationCounter').fadeOut();
});
});


});
</script>
@endpush
Expand Down
111 changes: 111 additions & 0 deletions app/Domain/Tickets/Controllers/CustomTemplates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Leantime\Domain\Tickets\Controllers;

use Leantime\Core\Controller\Controller;
use Leantime\Domain\Tickets\Services\CustomTemplatesService;
use Leantime\Core\Controller\Frontcontroller;


class CustomTemplates extends Controller
{
private CustomTemplatesService $templateService;

public function init(CustomTemplatesService $templateService)
{
$this->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;
}
}
66 changes: 66 additions & 0 deletions app/Domain/Tickets/Repositories/CustomTemplatesRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Leantime\Domain\Tickets\Repositories;

use Leantime\Core\Db\Db;

class CustomTemplatesRepository
{
private Db $db;

public function __construct(Db $db)
{
$this->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);
}
}
49 changes: 49 additions & 0 deletions app/Domain/Tickets/Services/CustomTemplatesService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Leantime\Domain\Tickets\Services;

use Leantime\Domain\Tickets\Repositories\CustomTemplatesRepository;

class CustomTemplatesService
{
private CustomTemplatesRepository $customTemplatesRepo;

public function __construct(CustomTemplatesRepository $customTemplatesRepo)
{
$this->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);
}
}
96 changes: 96 additions & 0 deletions app/Domain/Tickets/Templates/submodules/ticketDetails.sub.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,102 @@ class="autosave-field"
<textarea name="description" id="ticketDescription"
class="complexEditor"><?php echo $ticket->description !== null ? htmlentities($ticket->description) : ''; ?></textarea><br/>
</div>
<div class="form-group">
<button type="button" class="btn btn-sm btn-default" id="saveDescriptionAsTemplate">
<i class="fa fa-save"></i> Save as Template
</button>
</div>
<div id="saveTemplateModal" class="modal" style="display:none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" id="closeTemplateModal"><span>&times;</span></button>
<h4 class="modal-title"><i class="fa fa-save"></i> Save as Template</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label class="control-label">Template Name</label>
<input type="text" id="templateNameInput" class="form-control" placeholder="Enter template name..." autocomplete="off"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" id="closeTemplateModal2">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmSaveTemplate">
<i class="fa fa-save"></i> Save Template
</button>
</div>
</div>
</div>
</div>

<script>
jQuery(document).ready(function($) {

$('#saveDescriptionAsTemplate').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
$('#templateNameInput').val('');
$('#saveTemplateModal').modal('show');
});

$('#closeTemplateModal, #closeTemplateModal2').on('click', function() {
$('#saveTemplateModal').modal('hide');
});

$('#saveTemplateModal').on('shown.bs.modal', function() {
$('#templateNameInput').focus();
});

$('#templateNameInput').on('keypress', function(e) {
if (e.which === 13) {
$('#confirmSaveTemplate').trigger('click');
}
});

$('#confirmSaveTemplate').on('click', function() {
var templateName = $('#templateNameInput').val().trim();

if (!templateName) {
$('#templateNameInput').addClass('has-error').focus();
return;
}

var content = '';
if (typeof tinymce !== 'undefined' && tinymce.get('ticketDescription')) {
content = tinymce.get('ticketDescription').getContent();
} else {
content = $('#ticketDescription').val();
}

if (!content || content.trim() === '') {
$('#saveTemplateModal').modal('hide');
leantime.notifications.show('No content to save as template.', 'warning');
return;
}

$('#confirmSaveTemplate').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Saving...');

$.ajax({
url: '<?php echo BASE_URL; ?>/tickets/customTemplates/save',
method: 'POST',
data: {
title: templateName,
content: content
},
success: function(response) {
$('#saveTemplateModal').modal('hide');
alert('Template saved successfully!');
},
error: function(xhr, status, error) {
alert('error: ' + xhr.responseText.substring(0, 100));
},
complete: function() {
$('#confirmSaveTemplate').prop('disabled', false).html('<i class="fa fa-save"></i> Save Template');
}
});
});
});
</script>
<input type="hidden" name="acceptanceCriteria" value=""/>

</div>
Expand Down
Loading
Loading