Skip to content
This repository was archived by the owner on Oct 22, 2026. It is now read-only.
Closed
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
9 changes: 4 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: Deploy Leantime
on:
push:
branches:
- develop
- master
workflow_dispatch:
- develop
- master
workflow_dispatch:

jobs:
deploy:
Expand Down Expand Up @@ -56,7 +56,6 @@ jobs:
composer install --no-dev --optimize-autoloader --no-interaction

echo "Installing NPM dependencies..."
npm ci

echo "Building frontend assets..."
npx mix --production
Expand Down Expand Up @@ -141,7 +140,7 @@ jobs:
composer install --no-dev --optimize-autoloader --no-interaction

echo "Installing NPM dependencies..."
npm ci
npm ci

echo "Building frontend assets..."
npx mix --production
Expand Down
2 changes: 1 addition & 1 deletion app/Domain/Menu/Templates/headMenu.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class="active"
>{!! __('menu.company') !!}</a>
@else
<a
href="{{ BASE_URL }}/setting/editCompanySettings/"
href="{{ BASE_URL }}/timesheets/showAll/"
@if ($menuType == 'company')
class="active"
@endif
Expand Down
9 changes: 6 additions & 3 deletions app/Domain/Notifications/Services/Messengers.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Leantime\Core\Language as LanguageCore;
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
use Leantime\Domain\Setting\Repositories\Setting as SettingRepository;
use Leantime\Domain\Tickets\Services\Tickets;
use Leantime\Domain\Tickets\Services\Tickets as Tickets;

class Messengers
{
Expand Down Expand Up @@ -72,9 +72,12 @@ public function sendNotificationToMessengers(NotificationModel $notification, $p
*/
private function slackWebhook(NotificationModel $notification): bool
{
$slackWebhookURL = $this->settingsRepo->getSetting("projectsettings.{$notification->projectId}.slackWebhookURL");
$ticketService = app()->make(Tickets::class);
$statusLabelsArray = $ticketService->getStatusLabels($notification->projectId);

if ($slackWebhookURL !== '' && $slackWebhookURL !== false) {
$slackWebhookURL = $this->settingsRepo->getSetting("projectsettings.{$notification->projectId}.slackWebhookURL");

if ($slackWebhookURL !== '' && $slackWebhookURL !== false && !is_array($notification->entity) && (str_contains(strtolower($statusLabelsArray[$notification->entity->status]['name']), 'ready to test') || str_contains(strtolower($statusLabelsArray[$notification->entity->status]['name']), 'qa')) ) {
$message = $this->prepareMessage($notification);

$data = [
Expand Down
9 changes: 8 additions & 1 deletion app/Domain/Tickets/Controllers/ShowTicket.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
use Leantime\Domain\Users\Services\Users as UserService;
use Symfony\Component\HttpFoundation\Response;
use Leantime\Domain\Setting\Services\Setting as SettingService;

class ShowTicket extends Controller
{
Expand All @@ -39,7 +40,8 @@
FileService $fileService,
CommentService $commentService,
TimesheetService $timesheetService,
UserService $userService
UserService $userService,
SettingService $settingsService
): void {
$this->projectService = $projectService;
$this->ticketService = $ticketService;
Expand All @@ -48,6 +50,7 @@
$this->commentService = $commentService;
$this->timesheetService = $timesheetService;
$this->userService = $userService;
$this->settingsService = $settingsService;

Check failure on line 53 in app/Domain/Tickets/Controllers/ShowTicket.php

View workflow job for this annotation

GitHub Actions / phpstan

Access to an undefined property Leantime\Domain\Tickets\Controllers\ShowTicket::$settingsService.

Check failure on line 53 in app/Domain/Tickets/Controllers/ShowTicket.php

View workflow job for this annotation

GitHub Actions / phpstan

Access to an undefined property Leantime\Domain\Tickets\Controllers\ShowTicket::$settingsService.

if (session()->exists('lastPage') === false) {
session(['lastPage' => BASE_URL.'/tickets/showKanban']);
Expand Down Expand Up @@ -134,6 +137,10 @@
$this->tpl->assign('userInfo', $this->userService->getUser(session('userdata.id')));
$this->tpl->assign('users', $this->projectService->getUsersAssignedToProject($ticket->projectId));


$timezone = $this->settingsService->getSetting('usersettings.'.$this->userService->getUser(session('userdata.id'))['id'].'.timezone');

Check failure on line 141 in app/Domain/Tickets/Controllers/ShowTicket.php

View workflow job for this annotation

GitHub Actions / phpstan

Access to an undefined property Leantime\Domain\Tickets\Controllers\ShowTicket::$settingsService.

Check failure on line 141 in app/Domain/Tickets/Controllers/ShowTicket.php

View workflow job for this annotation

GitHub Actions / phpstan

Access to an undefined property Leantime\Domain\Tickets\Controllers\ShowTicket::$settingsService.
$this->tpl->assign('timezone', $timezone);

$projectData = $this->projectService->getProject($ticket->projectId);
$this->tpl->assign('projectData', $projectData);

Expand Down
138 changes: 138 additions & 0 deletions app/Domain/Tickets/Controllers/TicketHistoryController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

namespace Leantime\Domain\Tickets\Controllers;

use Leantime\Core\Controller\Controller;
use Leantime\Domain\Tickets\Models\TicketHistoryModel;
use Leantime\Domain\Auth\Services\Auth;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Leantime\Domain\Setting\Services\Setting as SettingService;

class TicketHistoryController extends Controller
{
private TicketHistoryModel $ticketHistoryModel;
private Auth $authService;
private SettingService $settingsService;

public function init(
TicketHistoryModel $ticketHistoryModel,
Auth $authService,
SettingService $settingsService
): void {
$this->ticketHistoryModel = $ticketHistoryModel;
$this->authService = $authService;
$this->settingsService = $settingsService;
}

public function logStatusChange(): JsonResponse
{
if (!$this->authService->loggedIn()) {
return new JsonResponse([
'success' => false,
'error' => 'Unauthorized'
], 401);
}

$ticketId = $_POST['ticketId'] ?? null;
$oldStatus = $_POST['oldStatus'] ?? '';
$newStatus = $_POST['newStatus'] ?? '';
$oldStatusText = $_POST['oldStatusText'] ?? '';
$newStatusText = $_POST['newStatusText'] ?? '';
$user = $_POST['user'] ?? $this->authService->getUserName();
$detailsAttributeId = $_POST['detailsAttributeId'] ?? '';

if (!$ticketId) {
return new JsonResponse([
'success' => false,
'error' => 'No ticket ID provided'
], 400);
}

try {
$insertId = $this->ticketHistoryModel->addStatusChange(
$ticketId,
$oldStatus,
$newStatus,
$oldStatusText,
$newStatusText,
$user,
$detailsAttributeId
);

if ($insertId) {
return new JsonResponse([
'success' => true,
'message' => 'Status change logged successfully',
'id' => $insertId
]);
} else {
return new JsonResponse([
'success' => false,
'error' => 'Failed to save status change'
], 500);
}
} catch (\Exception $e) {
return new JsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}

public function getStatusChanges(): Response
{
$ticketId = $_GET['ticketId'] ?? null;

if (!$ticketId) {
return new Response(
json_encode(['success' => false, 'error' => 'No ticket ID provided']),
400,
['Content-Type' => 'application/json']
);
}

try {
$changes = $this->ticketHistoryModel->getStatusChangesByTicket($ticketId);

if (empty($changes)) {
$html = '<p style="color: #999; padding: 10px;">No status changes yet.</p>';
} else {
$html = '<ul style="list-style: none; padding: 0; margin: 0;">';
foreach ($changes as $change) {
$detailsAttributeId = $change['detailsAttributeId'];
if($detailsAttributeId === 'priority') {
$attributeLabel = 'Priority';
} elseif($detailsAttributeId === 'storypoints') {
$attributeLabel = 'Effort';
} elseif($detailsAttributeId === 'editorId') {
$attributeLabel = 'Assigned to';
} elseif($detailsAttributeId === 'deadline') {
$attributeLabel = 'Due Date';
} elseif($detailsAttributeId === 'dueTime') {
$attributeLabel = 'Due Date Time';
} else {
$attributeLabel = 'Status';
}
$html .= '<li style="padding: 8px 0; border-bottom: 1px solid #eee;">';
$html .= '<strong>' . htmlspecialchars($change['changedBy']) . '</strong> changed '. $attributeLabel . ' on ';
$html .= '<span style="color: #666; font-size: 0.9em;">' . date('d.m.Y H:i', strtotime($change['changedAt'])) . '</span><br>';
$html .= '<span style="color: #999;">' . htmlspecialchars($change['oldStatusText']) . '</span>';
$html .= ' <i class="fa fa-arrow-right" style="color: #999; font-size: 0.8em;"></i> ';
$html .= '<span style="color: #28a745; font-weight: bold;">' . htmlspecialchars($change['newStatusText']) . '</span>';
$html .= '</li>';
}
$html .= '</ul>';
}

return new Response($html, 200, ['Content-Type' => 'text/html']);

} catch (\Exception $e) {
return new Response(
'<p style="color: red;">Error loading status changes: ' . htmlspecialchars($e->getMessage()) . '</p>',
500,
['Content-Type' => 'text/html']
);
}
}
}
74 changes: 74 additions & 0 deletions app/Domain/Tickets/Models/TicketHistoryModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Leantime\Domain\Tickets\Models;
use Leantime\Core\Db\Db as DbCore;
use PDO;

class TicketHistoryModel
{
private DbCore $db;

public function __construct(DbCore $db)
{
$this->db = $db;
}

public function addStatusChange($ticketId, $oldStatus, $newStatus, $oldStatusText, $newStatusText, $changedBy, $detailsAttributeId )
{
$sql = "INSERT INTO zp_ticket_status_changes
(ticketId, oldStatus, newStatus, oldStatusText, newStatusText, changedBy, changedAt, detailsAttributeId)
VALUES (:ticketId, :oldStatus, :newStatus, :oldStatusText, :newStatusText, :changedBy, NOW(), :detailsAttributeId)";

$stmn = $this->db->database->prepare($sql);
$stmn->bindValue(':ticketId', $ticketId, PDO::PARAM_INT);
$stmn->bindValue(':oldStatus', $oldStatus, PDO::PARAM_STR);
$stmn->bindValue(':newStatus', $newStatus, PDO::PARAM_STR);
$stmn->bindValue(':oldStatusText', $oldStatusText, PDO::PARAM_STR);
$stmn->bindValue(':newStatusText', $newStatusText, PDO::PARAM_STR);
$stmn->bindValue(':changedBy', $changedBy, PDO::PARAM_STR);
$stmn->bindValue(':detailsAttributeId', $detailsAttributeId, PDO::PARAM_STR);

if ($stmn->execute()) {
return $this->db->database->lastInsertId();
}

return false;
}

public function getStatusChangesByTicket($ticketId)
{
$sql = "SELECT * FROM zp_ticket_status_changes
WHERE ticketId = :ticketId
ORDER BY changedAt DESC";

$stmn = $this->db->database->prepare($sql);
$stmn->bindValue(':ticketId', $ticketId, PDO::PARAM_INT);
$stmn->execute();

return $stmn->fetchAll(PDO::FETCH_ASSOC);
}

public function getLatestStatusChange($ticketId)
{
$sql = "SELECT * FROM zp_ticket_status_changes
WHERE ticketId = :ticketId
ORDER BY changedAt DESC
LIMIT 1";

$stmn = $this->db->database->prepare($sql);
$stmn->bindValue(':ticketId', $ticketId, PDO::PARAM_INT);
$stmn->execute();

return $stmn->fetch(PDO::FETCH_ASSOC);
}

public function deleteStatusChangesByTicket($ticketId)
{
$sql = "DELETE FROM zp_ticket_status_changes WHERE ticketId = :ticketId";

$stmn = $this->db->database->prepare($sql);
$stmn->bindValue(':ticketId', $ticketId, PDO::PARAM_INT);

return $stmn->execute();
}
}
3 changes: 0 additions & 3 deletions app/Domain/Tickets/Repositories/TicketHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ class TicketHistory
{
private DbCore $db;

/**
* __construct - get database connection
*/
public function __construct(DbCore $db)
{
$this->db = $db;
Expand Down
2 changes: 1 addition & 1 deletion app/Domain/Tickets/Repositories/Tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Tickets

public array $priority = ['1' => 'Critical', '2' => 'High', '3' => 'Medium', '4' => 'Low', '5' => 'Lowest'];

public array $efforts = ['0.5' => '< 2min', '1' => 'XS', '2' => 'S', '3' => 'M', '5' => 'L', '8' => 'XL', '13' => 'XXL'];
public array $efforts = ['1' => '< 2min', '2' => 'XS', '3' => 'S', '4' => 'M', '5' => 'L', '6' => 'XL', '7' => 'XXL'];

public array $type = ['task', 'subtask', 'story', 'bug'];

Expand Down
Loading
Loading