Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .idea/leantime-oss.iml

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

1 change: 1 addition & 0 deletions app/Core/Middleware/AuthCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class AuthCheck
'cron.run',
'auth.callback',
'auth.redirect',
'api.timelogs',
];

public function __construct(
Expand Down
84 changes: 84 additions & 0 deletions app/Domain/Api/Controllers/Timelogs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Leantime\Domain\Api\Controllers;

use Leantime\Core\Controller\Controller;
use Leantime\Domain\Auth\Repositories\AccessTokenRepository;
use Leantime\Domain\Timesheets\Repositories\Timesheets as TimesheetsRepository;
use Leantime\Domain\Users\Repositories\Users as UsersRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class Timelogs extends Controller
{
private TimesheetsRepository $timesheetsRepo;
private UsersRepository $usersRepo;
private AccessTokenRepository $tokenRepo;

public function init(
TimesheetsRepository $timesheetsRepo,
UsersRepository $usersRepo,
AccessTokenRepository $tokenRepo,
): void {
$this->timesheetsRepo = $timesheetsRepo;
$this->usersRepo = $usersRepo;
$this->tokenRepo = $tokenRepo;
}

public function get(array $params): Response
{
$token = $this->incomingRequest->getBearerToken();
if (empty($token)) {
return new JsonResponse(['error' => 'Unauthorized'], 401);
}

$tokenRecord = $this->tokenRepo->findToken($token);
if (!$tokenRecord) {
return new JsonResponse(['error' => 'Unauthorized'], 401);
}

$email = trim($params['email'] ?? '');
$date = trim($params['date'] ?? '');

if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return new JsonResponse(['error' => 'Missing or invalid email parameter'], 400);
}

if (empty($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return new JsonResponse(['error' => 'Missing or invalid date parameter (expected YYYY-MM-DD)'], 400);
}

$user = $this->usersRepo->getUserByEmail($email);
if (!$user) {
return new JsonResponse(['error' => 'No user found for the given email'], 404);
}

$logs = $this->timesheetsRepo->getTimesheetsByUserAndDate((int) $user['id'], $date);

$grouped = [];
foreach ($logs as $log) {
$ticketName = '#'.$log['ticketId'].' - '.($log['headline'] ?? 'Unknown ticket');
$grouped[$ticketName][] = [
'duration_minutes' => (int) round((float) $log['hours'] * 60),
'description' => $log['description'] ?? '',
];
}

$data = array_map(
fn($ticketName, $entries) => [
'ticket_name' => $ticketName,
'logs' => $entries,
],
array_keys($grouped),
array_values($grouped)
);

$this->tokenRepo->updateLastUsedAt($tokenRecord['id']);

return new JsonResponse([
'email' => $email,
'date' => $date,
'data' => array_values($data),
]);
}
}
30 changes: 30 additions & 0 deletions app/Domain/Install/Repositories/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class Install
30408,
30409,
30410,
30411,
];

/**
Expand Down Expand Up @@ -476,6 +477,7 @@ private function sqlPrep(): string
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`projectKey` varchar(10) DEFAULT NULL,
`incrementalTicketId` tinyint(1) NOT NULL DEFAULT 1,
`clientId` int(100) DEFAULT NULL,
`details` text,
`state` int(2) DEFAULT NULL,
Expand Down Expand Up @@ -2123,6 +2125,34 @@ public function update_sql_30410(): bool|array
}


return count($errors) ? $errors : true;
}

public function update_sql_30411(): bool|array
{
$errors = [];

try {
$columnExists = $this->connection->select(
"SELECT COUNT(*) as count
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'zp_projects'
AND COLUMN_NAME = 'incrementalTicketId'"
);

if ($columnExists[0]->count == 0) {
$this->connection->statement(
'ALTER TABLE `zp_projects` ADD COLUMN `incrementalTicketId` TINYINT(1) NOT NULL DEFAULT 1 AFTER `projectKey`'
);
}
} catch (\Exception $e) {
Log::error('Failed to add incrementalTicketId column: '.$e->getMessage());
if (! str_contains($e->getMessage(), 'Duplicate column name')) {
array_push($errors, 'Failed to add incrementalTicketId column: '.$e->getMessage());
}
}

return count($errors) ? $errors : true;
}
}
17 changes: 14 additions & 3 deletions app/Domain/Notifications/Services/Messengers.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,16 @@ private function slackWebhook(NotificationModel $notification): bool
$statusLabelsArray = $ticketService->getStatusLabels($notification->projectId);

$slackWebhookURL = $this->settingsRepo->getSetting("projectsettings.{$notification->projectId}.slackWebhookURL");
$slackChannelId = $this->settingsRepo->getSetting("projectsettings.{$notification->projectId}.slackChannelId");

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')) ) {
$statusKey = !is_array($notification->entity) ? ($notification->entity->status ?? null) : null;
$statusLabel = $statusKey !== null && isset($statusLabelsArray[$statusKey]) ? $statusLabelsArray[$statusKey] : null;
$slackNotifyEnabled = $statusLabel !== null && !empty($statusLabel['slackNotify']);

$slackConfigured = ($slackWebhookURL !== '' && $slackWebhookURL !== false)
&& ($slackChannelId !== '' && $slackChannelId !== false);

if ($slackConfigured && !is_array($notification->entity) && $slackNotifyEnabled) {
$message = $this->prepareMessage($notification);

$entity = $notification->entity;
Expand All @@ -91,9 +99,12 @@ private function slackWebhook(NotificationModel $notification): bool

$title = $ticketNumber ? "$ticketNumber - $headline" : $headline;

$statusName = $statusLabel['name'] ?? '';
$pretext = session('userdata.name') . ' has moved the ticket to ' . $statusName;

$message[0] = [
'pretext' => session('userdata.name') . " has the ticket ready to test",
'fallback' => session('userdata.name') . " has the ticket ready to test",
'pretext' => $pretext,
'fallback' => $pretext,
'title' => $title,
'title_link' => $ticketUrl,
'color' => '#006d9f',
Expand Down
1 change: 1 addition & 0 deletions app/Domain/Projects/Controllers/ShowProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ public function run()
$values = [
'name' => $_POST['name'],
'projectKey' => $projectKey,
'incrementalTicketId' => isset($_POST['incrementalTicketId']) && $_POST['incrementalTicketId'] === '1',
'details' => $_POST['details'],
'clientId' => $_POST['clientId'],
'state' => $_POST['projectState'],
Expand Down
12 changes: 12 additions & 0 deletions app/Domain/Projects/Js/projectsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ leantime.projectsController = (function () {

jQuery('#todoStatusList').append("<li>" + statusCopy.html() + "</li>");

var todosettings = document.getElementById("todosettings");
if (todosettings) {
var slackEnabled = todosettings.getAttribute("data-slack-enabled") === "1";
var lastLine = todosettings.querySelector("#todoStatusList li:last-child");
var slackCheckbox = lastLine && lastLine.querySelector(".slack-notify-checkbox");
if (slackCheckbox && !slackEnabled) {
slackCheckbox.disabled = true;
var msg = todosettings.getAttribute("data-slack-channel-required-msg");
if (msg) { slackCheckbox.setAttribute("title", msg); }
}
}

jQuery("#todosettings select.colorChosen").chosen("destroy");
leantime.projectsController.initSelectFields();
jQuery("#todoStatusList").sortable("destroy");
Expand Down
4 changes: 4 additions & 0 deletions app/Domain/Projects/Repositories/Projects.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ public function getProject($id): array|bool
zp_projects.id,
zp_projects.name,
zp_projects.projectKey,
zp_projects.incrementalTicketId,
zp_projects.clientId,
zp_projects.details,
zp_projects.state,
Expand Down Expand Up @@ -666,6 +667,7 @@ public function editProject(array $values, $id): void
$query = 'UPDATE zp_projects SET
name = :name,
projectKey = :projectKey,
incrementalTicketId = :incrementalTicketId,
details = :details,
clientId = :clientId,
state = :state,
Expand All @@ -687,6 +689,8 @@ public function editProject(array $values, $id): void
$stmn->bindValue('name', $values['name'], PDO::PARAM_STR);
$projectKeyValue = $values['projectKey'] ?? null;
$stmn->bindValue('projectKey', $projectKeyValue, $projectKeyValue === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
$incrementalTicketId = isset($values['incrementalTicketId']) && $values['incrementalTicketId'] ? 1 : 0;
$stmn->bindValue('incrementalTicketId', $incrementalTicketId, PDO::PARAM_INT);
$stmn->bindValue('details', $values['details'] ?? '', PDO::PARAM_STR);
$stmn->bindValue('clientId', $values['clientId'] ?? '', PDO::PARAM_STR);
$stmn->bindValue('state', $values['state'] ?? '', PDO::PARAM_STR);
Expand Down
25 changes: 18 additions & 7 deletions app/Domain/Projects/Templates/showProject.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@

</div>

<div id="todosettings">
<div id="todosettings" data-slack-enabled="<?= !empty($tpl->get('slackChannelId')) ? '1' : '0'; ?>" data-slack-channel-required-msg="<?= $tpl->e($tpl->__('text.slack_channel_required_for_notify')); ?>">
<form action="<?= BASE_URL ?>/projects/showProject/<?php echo $project['id']; ?>#todosettings" method="post">
<ul class="sortableTicketList" id="todoStatusList">
<?php foreach ($tpl->get('todoStatus') as $key => $ticketStatus) { ?>
Expand Down Expand Up @@ -367,12 +367,19 @@
<label for=""><?= $tpl->__('label.showInKanban'); ?></label>
<input type="checkbox" name="labelKanbanCol-<?= $key?>" id="labelKanbanCol-<?= $key?>" <?= $ticketStatus['kanbanCol'] ? 'checked="checked"' : ''; ?>/>
</div>
<div class="remove">
<br />
<div class="remove col-md-1" style="min-width: 0; padding-left: 0; display: flex; flex-direction: column; justify-content: center;">
<label style="height: 1.2em; margin-bottom: 0.25em; visibility: hidden;">&nbsp;</label>
<?php if ($key != -1) { ?>
<a href="javascript:void(0);" onclick="leantime.projectsController.removeStatus(<?= $key?>)" class="delete"><span class="fa fa-trash"></span></a>
<a href="javascript:void(0);" onclick="leantime.projectsController.removeStatus(<?= $key?>)" class="delete" style="display: inline-block; line-height: 1;"><span class="fa fa-trash"></span></a>
<?php } ?>
</div>

<div class="col-md-1">
<?php $slackChannelId = $tpl->get('slackChannelId'); $slackEnabled = !empty($slackChannelId); ?>
<label for="labelSlackNotify-<?= $key?>"><?= $tpl->__('label.notify_slack'); ?></label>
<input type="checkbox" name="labelSlackNotify-<?= $key?>" id="labelSlackNotify-<?= $key?>" value="1" <?= !empty($ticketStatus['slackNotify']) ? 'checked="checked"' : ''; ?> <?= !$slackEnabled ? 'disabled title="'. $tpl->e($tpl->__('text.slack_channel_required_for_notify')) .'"' : ''; ?>/>
</div>

</div>

<?php if ($key == -1) { ?>
Expand Down Expand Up @@ -444,9 +451,13 @@
<label for=""><?= $tpl->__('label.showInKanban'); ?></label>
<input type="checkbox" name="labelKanbanCol-XXNEWKEYXX" id="labelKanbanCol-XXNEWKEYXX"/>
</div>
<div class="remove">
<br />
<a href="javascript:void(0);" onclick="leantime.projectsController.removeStatus('XXNEWKEYXX')" class="delete"><span class="fa fa-trash"></span></a>
<div class="col-md-1 newStatusSlackNotifyCol">
<label for="labelSlackNotify-XXNEWKEYXX"><?= $tpl->__('label.notify_slack'); ?></label>
<input type="checkbox" name="labelSlackNotify-XXNEWKEYXX" id="labelSlackNotify-XXNEWKEYXX" value="1" class="slack-notify-checkbox"/>
</div>
<div class="remove col-md-1" style="min-width: 0; padding-left: 0; display: flex; flex-direction: column; justify-content: flex-end;">
<label style="height: 1.2em; margin-bottom: 0.25em; visibility: hidden;">&nbsp;</label>
<a href="javascript:void(0);" onclick="leantime.projectsController.removeStatus('XXNEWKEYXX')" class="delete" style="display: inline-block; line-height: 1;"><span class="fa fa-trash"></span></a>
</div>
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions app/Domain/Projects/Templates/submodules/projectDetails.sub.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group" style="margin-bottom: 1rem;">
<div style="display: flex; align-items: center; gap: 0.75rem; flex-wrap: nowrap;">
<input type="checkbox" name="incrementalTicketId" id="incrementalTicketId" value="1"
<?php if (!empty($project['incrementalTicketId'])) { echo ' checked="checked"'; } ?>
style="margin: 0; flex-shrink: 0; width: 1.25rem; height: 1.25rem;" />
<label for="incrementalTicketId" style="font-weight: 600; margin: 0; cursor: pointer; white-space: nowrap; flex-shrink: 0;">
<?php $incCheckLabel = $tpl->__('label.incremental_ticket_id_enable'); echo $incCheckLabel === 'label.incremental_ticket_id_enable' ? 'Enable incremental project ticket numbering' : $incCheckLabel; ?>
</label>
</div>
<p class="text-muted" style="display">
<?php $incDesc = $tpl->__('label.incremental_ticket_id_description'); echo $incDesc === 'label.incremental_ticket_id_description' ? 'Incremental ticket numbering (1, 2, 3...) for this project. When disabled, tickets use a different ID scheme.' : $incDesc; ?>
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p>
Expand Down
6 changes: 6 additions & 0 deletions app/Domain/Tickets/Models/Tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class Tickets

public ?string $projectKey = null;

public mixed $incrementalTicketId = null;

public mixed $projectTicketNumber = null;

public ?string $clientName = '';

public ?string $userFirstname = '';
Expand Down Expand Up @@ -125,6 +129,8 @@ public function __construct(array|bool $values = false)
$this->milestoneid = $values['milestoneid'] ?? '';
$this->projectName = $values['projectName'] ?? '';
$this->projectKey = $values['projectKey'] ?? null;
$this->incrementalTicketId = $values['incrementalTicketId'] ?? null;
$this->projectTicketNumber = $values['projectTicketNumber'] ?? null;
$this->clientName = $values['clientName'] ?? '';
$this->userFirstname = $values['userFirstname'] ?? '';
$this->userLastname = $values['userLastname'] ?? '';
Expand Down
Loading
Loading