OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Tickets\Permissions\TicketsPermissions;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class DelMilestone extends Controller
{
private TicketService $ticketService;
public function init(TicketService $ticketService): void
{
$this->ticketService = $ticketService;
}
/**
* @throws BindingResolutionException
*/
#[RequiresPermission(TicketsPermissions::DELETE)]
public function get(): Response
{
if (! isset($_GET['id'])) {
return $this->tpl->displayPartial('errors.error404', responseCode: 404);
}
$id = (int) $_GET['id'];
$this->tpl->assign('ticket', $this->ticketService->getTicket($id));
return $this->tpl->displayPartial('tickets.delMilestone');
}
/**
* @throws BindingResolutionException
*/
#[RequiresPermission(TicketsPermissions::DELETE)]
public function post($params): Response
{
if (! isset($_GET['id'], $params['del'])) {
return $this->tpl->displayPartial('errors.error404', responseCode: 404);
}
if (($result = $this->ticketService->deleteMilestone($id = (int) ($_GET['id']))) === true) {
$this->tpl->setNotification($this->language->__('notification.milestone_deleted'), 'success');
return Frontcontroller::redirect(BASE_URL.'/tickets/roadmap');
}
$this->tpl->setNotification($this->language->__($result['msg']), 'error');
$this->tpl->assign('ticket', $this->ticketService->getTicket($id));
return $this->tpl->displayPartial('tickets.delMilestone');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Tickets\Permissions\TicketsPermissions;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class DelTicket extends Controller
{
private TicketService $ticketService;
public function init(TicketService $ticketService): void
{
$this->ticketService = $ticketService;
}
/**
* @throws \Exception
*/
#[RequiresPermission(TicketsPermissions::DELETE)]
public function get(): Response
{
if (! isset($_GET['id'])) {
return $this->tpl->display('errors.error404', responseCode: 404);
}
$id = (int) $_GET['id'];
try {
$this->ticketService->canDelete($id);
} catch (\Exception $e) {
$this->tpl->assign('error', $e->getMessage());
return $this->tpl->displayPartial('tickets.delTicket');
}
$this->tpl->assign('error', '');
$this->tpl->assign('ticket', $this->ticketService->getTicket($id));
return $this->tpl->displayPartial('tickets.delTicket');
}
/**
* @throws \Exception
*/
#[RequiresPermission(TicketsPermissions::DELETE)]
public function post($params): Response
{
if (! isset($params['del'])) {
return $this->tpl->display('errors.error403', responseCode: 403);
}
if (! isset($_GET['id'])) {
return $this->tpl->display('errors.error404', responseCode: 404);
}
$id = (int) $_GET['id'];
$result = $this->ticketService->delete($id);
if ($result === true) {
$this->tpl->setNotification($this->language->__('notification.todo_deleted'), 'success');
$redirect = session('lastPage') ?? BASE_URL.'/';
return Frontcontroller::redirect($redirect);
}
$this->tpl->setNotification($this->language->__($result['msg']), 'error');
$this->tpl->assign('ticket', $this->ticketService->getTicket($id));
return $this->tpl->displayPartial('tickets.delTicket');
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Comments\Services\Comments as CommentService;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
class EditMilestone extends Controller
{
private TicketService $ticketService;
private CommentService $commentsService;
private ProjectService $projectService;
/**
* init - initialize private variables
*/
public function init(
TicketService $ticketService,
CommentService $commentsService,
ProjectService $projectService
): void {
$this->ticketService = $ticketService;
$this->commentsService = $commentsService;
$this->projectService = $projectService;
}
/**
* get - handle get requests
*/
public function get($params)
{
if (isset($params['id'])) {
// Delete comment
if (isset($params['delComment']) === true) {
$commentId = (int) ($params['delComment']);
$this->commentsService->deleteComment($commentId);
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success');
}
$milestone = $this->ticketService->getMilestone((int) $params['id']);
if ($milestone === false || ! isset($milestone->id)) {
$this->tpl->setNotification($this->language->__('notifications.could_not_find_milestone'), 'error');
return Frontcontroller::redirect(BASE_URL.'/tickets/roadmap/');
}
// Ensure this ticket belongs to the current project
if (session('currentProject') != $milestone->projectId) {
$this->projectService->changeCurrentSessionProject($milestone->projectId);
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/'.$milestone->id);
}
$comments = $this->commentsService->getComments('ticket', $params['id']);
} else {
$milestone = $this->ticketService->getNewMilestone();
$comments = [];
}
$allAssignedprojects = $this->projectService->getProjectsAssignedToUser(session('userdata.id'), 'open');
$this->tpl->assign('allAssignedprojects', $allAssignedprojects);
$this->tpl->assign('statusLabels', $this->ticketService->getStatusLabels());
$this->tpl->assign('comments', $comments);
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]);
$this->tpl->assign('milestones', $allProjectMilestones);
$this->tpl->assign('users', $this->projectService->getUsersWithAccessToProject((int) session('currentProject')));
$this->tpl->assign('milestone', $milestone);
return $this->tpl->displayPartial('tickets.milestoneDialog');
}
/**
* post - handle post requests
*/
public function post($params)
{
// If ID is set its an update
if (isset($_GET['id']) && (int) $_GET['id'] > 0) {
$params['id'] = (int) $_GET['id'];
if (isset($params['comment']) === true) {
$milestone = $this->ticketService->getMilestone($params['id']);
if ($this->ticketService->addMilestoneComment($params, $milestone)) {
$this->tpl->setNotification($this->language->__('notifications.comment_added_successfully'), 'success');
} else {
$this->tpl->setNotification($this->language->__('notifications.problem_saving_your_comment'), 'error');
}
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/'.$params['id']);
}
if (isset($params['headline']) === true) {
if ($this->ticketService->updateMilestoneFromDialog($params)) {
$this->tpl->setNotification($this->language->__('notification.milestone_edited_successfully'), 'success');
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/'.$params['id'].'?closeModal=1');
}
$this->tpl->setNotification($this->language->__('notification.saving_milestone_error'), 'error');
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/'.$params['id']);
}
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/'.$params['id']);
}
$result = $this->ticketService->createMilestoneFromDialog($params);
if (is_numeric($result)) {
$this->tpl->setNotification($this->language->__('notification.milestone_created_successfully'), 'success');
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/'.$result.'?closeModal=1');
}
$this->tpl->setNotification($this->language->__('notification.saving_milestone_error'), 'error');
return Frontcontroller::redirect(BASE_URL.'/tickets/editMilestone/');
}
/**
* put - handle put requests
*/
public function put($params) {}
/**
* delete - handle delete requests
*/
public function delete($params) {}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Tickets\Permissions\TicketsPermissions;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class MoveTicket extends Controller
{
private TicketService $ticketService;
private ProjectService $projectService;
public function init(
TicketService $ticketService,
ProjectService $projectService
): void {
$this->ticketService = $ticketService;
$this->projectService = $projectService;
}
/**
* @throws BindingResolutionException
*/
#[RequiresPermission(TicketsPermissions::EDIT)]
public function get($params): Response
{
$ticketId = $params['id'] ?? '';
$ticket = $this->ticketService->getTicket($ticketId);
if (! $ticket) {
return $this->tpl->displayPartial('errors.error404', responseCode: 404);
}
$projects = $this->projectService->getProjectsAssignedToUser(session('userdata.id'));
$this->tpl->assign('ticket', $ticket);
$this->tpl->assign('projects', $projects);
return $this->tpl->displayPartial('tickets.moveTicket');
}
/**
* @throws BindingResolutionException
*/
#[RequiresPermission(TicketsPermissions::EDIT)]
public function post($params): Response
{
if (! empty($ticketId = (int) $_GET['id']) && ! empty($projectId = (int) $params['projectId'])) {
if ($this->ticketService->moveTicket($ticketId, $projectId)) {
$this->tpl->setNotification($this->language->__('text.ticket_moved'), 'success');
} else {
$this->tpl->setNotification($this->language->__('text.move_problem'), 'error');
}
}
return FrontcontrollerCore::redirect(BASE_URL.'/tickets/moveTicket/'.$ticketId.'?closeModal=true');
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Core\Support\FromFormat;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Sprints\Services\Sprints as SprintService;
use Leantime\Domain\Tickets\Models\Tickets as TicketModel;
use Leantime\Domain\Tickets\Permissions\TicketsPermissions;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
use Leantime\Domain\Users\Services\Users as UserService;
use Symfony\Component\HttpFoundation\Response;
class NewTicket extends Controller
{
private ProjectService $projectService;
private TicketService $ticketService;
private SprintService $sprintService;
private TimesheetService $timesheetService;
private UserService $userService;
public function init(
ProjectService $projectService,
TicketService $ticketService,
SprintService $sprintService,
TimesheetService $timesheetService,
UserService $userService
): void {
$this->projectService = $projectService;
$this->ticketService = $ticketService;
$this->sprintService = $sprintService;
$this->timesheetService = $timesheetService;
$this->userService = $userService;
if (! session()->exists('lastPage')) {
session(['lastPage' => BASE_URL.'/tickets/showKanban/']);
}
}
/**
* @throws BindingResolutionException
*/
#[RequiresPermission(TicketsPermissions::CREATE)]
public function get(): Response
{
$ticket = app()->make(TicketModel::class, [
'values' => [
'userLastname' => session('userdata.name'),
'status' => 3,
'projectId' => session('currentProject'),
'sprint' => session('currentSprint') ?? '',
'editorId' => session('userdata.id'),
],
]);
$ticket->date = dtHelper()->userNow();
$this->tpl->assign('ticket', $ticket);
$this->tpl->assign('ticketParents', $this->ticketService->getAllPossibleParents($ticket));
$this->tpl->assign('statusLabels', $this->ticketService->getStatusLabels());
$this->tpl->assign('ticketTypes', $this->ticketService->getTicketTypes());
$this->tpl->assign('efforts', $this->ticketService->getEffortLabels());
$this->tpl->assign('priorities', $this->ticketService->getPriorityLabels());
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]);
$this->tpl->assign('milestones', $allProjectMilestones);
$this->tpl->assign('sprints', $this->sprintService->getAllSprints(session('currentProject')));
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
$this->tpl->assign('ticketHours', 0);
$this->tpl->assign('userHours', 0);
$this->tpl->assign('timesheetsAllHours', 0);
$this->tpl->assign('remainingHours', 0);
$this->tpl->assign('userInfo', $this->userService->getUser(session('userdata.id')));
$this->tpl->assign('users', $this->projectService->getUsersWithAccessToProject((int) session('currentProject')));
$allAssignedprojects = $this->projectService->getProjectsUserHasAccessTo(session('userdata.id'));
$this->tpl->assign('allAssignedprojects', $allAssignedprojects);
return $this->tpl->displayPartial('tickets.newTicketModal');
}
/**
* @throws BindingResolutionException
*/
#[RequiresPermission(TicketsPermissions::CREATE)]
public function post($params): Response
{
if (isset($params['saveTicket']) || isset($params['saveAndCloseTicket'])) {
$params['timeToFinish'] = format(value: $params['timeToFinish'] ?? '', fromFormat: FromFormat::User24hTime)->userTime24toUserTime();
$params['timeFrom'] = format(value: $params['timeFrom'] ?? '', fromFormat: FromFormat::User24hTime)->userTime24toUserTime();
$params['timeTo'] = format(value: $params['timeTo'] ?? '', fromFormat: FromFormat::User24hTime)->userTime24toUserTime();
$result = $this->ticketService->addTicket($params);
if (is_array($result) === false) {
$this->tpl->setNotification($this->language->__('notifications.ticket_saved'), 'success');
if (isset($params['saveAndCloseTicket']) === true && $params['saveAndCloseTicket'] == 1) {
return Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$result.'?closeModal=1');
} else {
return Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$result);
}
} else {
$this->tpl->setNotification($this->language->__($result['msg']), 'error');
$ticket = app()->makeWith(TicketModel::class, ['values' => $params]);
$ticket->userLastname = session('userdata.name');
$this->tpl->assign('ticket', $ticket);
$this->tpl->assign('ticketParents', $this->ticketService->getAllPossibleParents($ticket));
$this->tpl->assign('statusLabels', $this->ticketService->getStatusLabels());
$this->tpl->assign('ticketTypes', $this->ticketService->getTicketTypes());
$this->tpl->assign('efforts', $this->ticketService->getEffortLabels());
$this->tpl->assign('priorities', $this->ticketService->getPriorityLabels());
$this->tpl->assign('milestones', $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]));
$this->tpl->assign('sprints', $this->sprintService->getAllSprints(session('currentProject')));
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
$this->tpl->assign('ticketHours', 0);
$this->tpl->assign('userHours', 0);
$this->tpl->assign('timesheetsAllHours', 0);
$this->tpl->assign('remainingHours', 0);
$this->tpl->assign('userInfo', $this->userService->getUser(session('userdata.id')));
$this->tpl->assign('users', $this->projectService->getUsersWithAccessToProject((int) session('currentProject')));
$allAssignedprojects = $this->projectService->getProjectsUserHasAccessTo(session('userdata.id'));
$this->tpl->assign('allAssignedprojects', $allAssignedprojects);
return $this->tpl->displayPartial('tickets.newTicketModal');
}
}
return Frontcontroller::redirect(BASE_URL.'/tickets/newTicket');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
class Roadmap extends Controller
{
private TicketService $ticketService;
/**
* init - initialize private variables
*/
public function init(
TicketService $ticketService
): void {
$this->ticketService = $ticketService;
session(['lastPage' => CURRENT_URL]);
session(['lastMilestoneView' => 'timeline']);
session(['lastFilterdMilestonesView' => CURRENT_URL]);
}
/**
* get - handle get requests
*/
public function get($params)
{
$params = $this->ticketService->normalizeRoadmapParams($params);
// Sets the filter module to show a quick toggle for task types
$this->tpl->assign('enableTaskTypeToggle', true);
$this->tpl->assign('showTasks', $params['showTasks'] ?? 'false');
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
$allProjectMilestones = $this->ticketService->getAllMilestones($template_assignments['searchCriteria'], 'standard');
$allProjectMilestones = $this->ticketService->getBulkMilestoneProgress($allProjectMilestones);
$this->tpl->assign('timelineTasks', $allProjectMilestones);
return $this->tpl->display('tickets.roadmap');
}
/**
* post - handle post requests
*/
public function post($params)
{
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
$allProjectMilestones = $this->ticketService->getAllMilestones($template_assignments['searchCriteria']);
$this->tpl->assign('timelineTasks', $allProjectMilestones);
return $this->tpl->display('tickets.roadmap');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Domain\Clients\Services\Clients as ClientService;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
class RoadmapAll extends Controller
{
private TicketService $ticketService;
private ClientService $clientService;
/**
* init - initialize private variables
*/
public function init(
ClientService $clientService,
TicketService $ticketService
): void {
$this->clientService = $clientService;
$this->ticketService = $ticketService;
}
/**
* get - handle get requests
*/
public function get($params)
{
$clientId = 0;
$currentClientName = '';
if (isset($_GET['client']) === true && $_GET['client'] != '') {
$clientId = (int) $_GET['client'];
$currentClientName = $this->ticketService->getClientNameById($clientId);
}
$allProjectMilestones = $this->ticketService->getAllMilestonesOverview(false, 'date', false, $clientId);
$allClients = $this->clientService->getUserClients(session('userdata.id'));
$this->tpl->assign('currentClientName', $currentClientName);
$this->tpl->assign('currentClient', $clientId);
$this->tpl->assign('milestones', $allProjectMilestones);
$this->tpl->assign('clients', $allClients);
return $this->tpl->display('tickets.roadmapAll');
}
/**
* post - handle post requests
*/
public function post($params)
{
$allProjectMilestones = $this->ticketService->getAllMilestonesOverview();
$this->tpl->assign('milestones', $allProjectMilestones);
return $this->tpl->display('tickets.roadmapAll');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class ShowAll extends Controller
{
private TicketService $ticketService;
public function init(
TicketService $ticketService,
): void {
$this->ticketService = $ticketService;
session(['lastPage' => CURRENT_URL]);
session(['lastTicketView' => 'table']);
session(['lastFilterdTicketTableView' => CURRENT_URL]);
if (! session()->exists('currentProjectName')) {
Frontcontroller::redirect(BASE_URL.'/');
}
}
/**
* @throws \Exception
*/
public function get($params): Response
{
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
return $this->tpl->display('tickets.showAll');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class ShowAllMilestones extends Controller
{
private TicketService $ticketService;
public function init(
TicketService $ticketService
): void {
$this->ticketService = $ticketService;
session(['lastPage' => CURRENT_URL]);
session(['lastMilestoneView' => 'milestonetable']);
session(['lastFilterdMilestoneView' => CURRENT_URL]);
}
/**
* @throws \Exception
*/
public function get($params): Response
{
$params = $this->ticketService->normalizeRoadmapParams($params);
// Sets the filter module to show a quick toggle for task types
$this->tpl->assign('enableTaskTypeToggle', true);
$this->tpl->assign('showTasks', $params['showTasks'] ?? 'false');
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
return $this->tpl->display('tickets.showAllMilestones');
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Domain\Clients\Services\Clients as ClientService;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Leantime\Domain\Users\Services\Users as UserService;
use Symfony\Component\HttpFoundation\Response;
class ShowAllMilestonesOverview extends Controller
{
private TicketService $ticketService;
private UserService $userService;
private ClientService $clientService;
public function init(
TicketService $ticketService,
UserService $userService,
ClientService $clientService
): void {
$this->ticketService = $ticketService;
$this->userService = $userService;
$this->clientService = $clientService;
session(['lastPage' => CURRENT_URL]);
}
/**
* @throws \Exception
*/
public function get($params): Response
{
$clientId = 0;
$currentClientName = '';
if (isset($_GET['client']) === true && $_GET['client'] != '') {
$clientId = (int) $_GET['client'];
$currentClientName = $this->ticketService->getClientNameById($clientId);
}
$searchCriteria = $this->ticketService->getMilestonesOverviewSearchCriteria($params);
$this->tpl->assign('allTickets', $this->ticketService->getAllMilestonesOverview(false, 'duedate', false, $clientId, $searchCriteria));
$this->tpl->assign('allTicketStates', $this->ticketService->getStatusLabels());
$this->tpl->assign('efforts', $this->ticketService->getEffortLabels());
$this->tpl->assign('priorities', $this->ticketService->getPriorityLabels());
$this->tpl->assign('ticketTypeIcons', $this->ticketService->getTypeIcons());
$this->tpl->assign('searchCriteria', $searchCriteria);
$this->tpl->assign('numOfFilters', $this->ticketService->countSetFilters($searchCriteria));
$allClients = $this->clientService->getUserClients(session('userdata.id'));
$this->tpl->assign('clients', $allClients);
$this->tpl->assign('currentClientName', $currentClientName);
$this->tpl->assign('currentClient', $clientId);
$this->tpl->assign('users', $this->userService->getAll());
$this->tpl->assign('milestones', $this->ticketService->getAllMilestones([
'sprint' => '',
'type' => 'milestone',
'currentProject' => session('currentProject'),
]));
$this->tpl->assign('types', $this->ticketService->getTicketTypes());
return $this->tpl->display('tickets.showAllMilestonesOverview');
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class ShowKanban extends Controller
{
private TicketService $ticketService;
public function init(
TicketService $ticketService
): void {
$this->ticketService = $ticketService;
session(['lastPage' => CURRENT_URL]);
session(['lastTicketView' => 'kanban']);
session(['lastFilterdTicketKanbanView' => CURRENT_URL]);
}
/**
* @throws \Exception
*/
public function get(array $params): Response
{
// Status groupBy is redundant on Kanban (status already shown as columns)
// Auto-reset to "all" (no grouping) for cleaner default view
if (isset($params['groupBy']) && $params['groupBy'] === 'status') {
$params['groupBy'] = 'all';
}
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
$allKanbanColumns = $this->ticketService->getKanbanColumns();
// NEW: Calculate status breakdown for swimlane visualizations
$statusBreakdown = $this->ticketService->getStatusBreakdownBySwimlane(
$template_assignments['allTickets'],
$allKanbanColumns
);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
$this->tpl->assign('allKanbanColumns', $allKanbanColumns);
$this->tpl->assign('statusBreakdown', $statusBreakdown);
return $this->tpl->display('tickets.showKanban');
}
/**
* @throws BindingResolutionException
*/
public function post(array $params): Response
{
// QuickAdd
if (isset($_POST['quickadd']) && $_POST['quickadd'] == 1) {
$formParams = [
'headline' => $_POST['headline'] ?? '',
'status' => $_POST['status'] ?? '',
'milestone' => $_POST['milestone'] ?? '',
'sprint' => $_POST['sprint'] ?? '',
'projectId' => session('currentProject'),
'editorId' => session('userdata.id'),
];
$swimlaneValue = $_POST['swimlane'] ?? null;
$groupBy = $_POST['groupBy'] ?? null;
$stayOpen = isset($_POST['stay_open']) && $_POST['stay_open'] === '1';
$result = $this->ticketService->quickAddTicketFromKanban($formParams, $swimlaneValue, $groupBy, $stayOpen);
if ($result['success'] === false) {
$this->tpl->setNotification($result['message'], 'error');
} else {
$this->tpl->setNotification('Task created: '.htmlspecialchars($result['headline']), 'success');
}
return Frontcontroller::redirect(CURRENT_URL.'#status-'.$result['status']);
}
return Frontcontroller::redirect(CURRENT_URL);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Symfony\Component\HttpFoundation\Response;
class ShowList extends Controller
{
private TicketService $ticketService;
public function init(
TicketService $ticketService
): void {
$this->ticketService = $ticketService;
session(['lastPage' => CURRENT_URL]);
session(['lastTicketView' => 'list']);
session(['lastFilterdTicketListView' => CURRENT_URL]);
}
/**
* @throws \Exception
*/
public function get($params): Response
{
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
return $this->tpl->display('tickets.showList');
}
/**
* @throws BindingResolutionException
*/
public function post(array $params): Response
{
// QuickAdd
if (isset($_POST['quickadd'])) {
$formParams = [
'headline' => $_POST['headline'] ?? '',
'milestone' => $_POST['milestone'] ?? '',
'sprint' => $_POST['sprint'] ?? '',
'projectId' => session('currentProject'),
'editorId' => session('userdata.id'),
];
$result = $this->ticketService->quickAddTicket($formParams);
if (is_array($result)) {
$this->tpl->setNotification($result['message'], $result['status']);
}
}
return Frontcontroller::redirect(CURRENT_URL);
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* showAll Class - show My Calender
*/
namespace Leantime\Domain\Tickets\Controllers;
use Leantime\Core\Controller\Controller;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
class ShowProjectCalendar extends Controller
{
private TicketService $ticketService;
/**
* init - initialize private variables
*/
public function init(
TicketService $ticketService
): void {
$this->ticketService = $ticketService;
session(['lastPage' => CURRENT_URL]);
session(['lastMilestoneView' => 'calendar']);
}
/**
* get - handle get requests
*/
public function get($params)
{
$template_assignments = $this->ticketService->getTicketTemplateAssignments($params);
array_map([$this->tpl, 'assign'], array_keys($template_assignments), array_values($template_assignments));
$allProjectMilestones = $this->ticketService->getAllMilestones($template_assignments['searchCriteria']);
$this->tpl->assign('milestones', $allProjectMilestones);
return $this->tpl->display('tickets.calendar');
}
/**
* post - handle post requests
*/
public function post($params)
{
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]);
$this->tpl->assign('milestones', $allProjectMilestones);
return $this->tpl->display('tickets.roadmap');
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Carbon\Carbon;
use Illuminate\Contracts\Container\BindingResolutionException;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Core\Support\FromFormat;
use Leantime\Domain\Comments\Services\Comments as CommentService;
use Leantime\Domain\Files\Services\Files as FileService;
use Leantime\Domain\Projects\Services\Projects as ProjectService;
use Leantime\Domain\Sprints\Services\Sprints as SprintService;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use Leantime\Domain\Tickets\Services\TicketResource as TicketResourceService;
use Leantime\Domain\Timesheets\Services\Timesheets as TimesheetService;
use Leantime\Domain\Users\Services\Users as UserService;
use Symfony\Component\HttpFoundation\Response;
class ShowTicket extends Controller
{
private ProjectService $projectService;
private TicketService $ticketService;
private SprintService $sprintService;
private FileService $fileService;
private CommentService $commentService;
private TimesheetService $timesheetService;
private UserService $userService;
private TicketResourceService $ticketResourceService;
public function init(
ProjectService $projectService,
TicketService $ticketService,
SprintService $sprintService,
FileService $fileService,
CommentService $commentService,
TimesheetService $timesheetService,
UserService $userService,
TicketResourceService $ticketResourceService
): void {
$this->projectService = $projectService;
$this->ticketService = $ticketService;
$this->sprintService = $sprintService;
$this->fileService = $fileService;
$this->commentService = $commentService;
$this->timesheetService = $timesheetService;
$this->userService = $userService;
$this->ticketResourceService = $ticketResourceService;
if (session()->exists('lastPage') === false) {
session(['lastPage' => BASE_URL.'/tickets/showKanban']);
}
}
/**
* @throws BindingResolutionException
*/
public function get($params): Response
{
if (! isset($params['id'])) {
return $this->tpl->displayPartial('errors.error400', responseCode: 400);
}
$id = (int) ($params['id']);
$ticket = $this->ticketService->getTicket($id);
if ($ticket === false) {
return $this->tpl->display('errors.error500', responseCode: 500);
}
// Ensure this ticket belongs to the current project
if (session('currentProject') != $ticket->projectId) {
$this->projectService->changeCurrentSessionProject($ticket->projectId);
return Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$id);
}
// Delete file
if (isset($params['delFile']) === true) {
if ($result = $this->fileService->deleteFile($params['delFile'])) {
$this->tpl->setNotification($this->language->__('notifications.file_deleted'), 'success');
return Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$id.'#files');
}
$this->tpl->setNotification($this->language->__('notifications.file_deleted_error'), 'error');
}
// Delete comment
if (isset($params['delComment']) === true) {
$commentId = (int) ($params['delComment']);
if ($this->commentService->deleteComment($commentId)) {
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success');
$response = Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$id);
$response->headers->set('HX-Trigger', 'ticketUpdate');
return $response;
}
$this->tpl->setNotification($this->language->__('notifications.comment_deleted_error'), 'error');
}
// Delete Subtask
if (isset($params['delSubtask']) === true) {
}
$this->tpl->assign('ticket', $ticket);
$this->tpl->assign('ticketParents', $this->ticketService->getAllPossibleParents($ticket));
$this->tpl->assign('statusLabels', $this->ticketService->getStatusLabels());
$this->tpl->assign('ticketTypes', $this->ticketService->getTicketTypes());
$this->tpl->assign('ticketTypeIcons', $this->ticketService->getTypeIcons());
$this->tpl->assign('efforts', $this->ticketService->getEffortLabels());
$this->tpl->assign('priorities', $this->ticketService->getPriorityLabels());
$allProjectMilestones = $this->ticketService->getAllMilestones([
'sprint' => '',
'type' => 'milestone',
'currentProject' => session('currentProject'),
]);
$this->tpl->assign('milestones', $allProjectMilestones);
$this->tpl->assign('sprints', $this->sprintService->getAllSprints(session('currentProject')));
$this->tpl->assign('kind', $this->timesheetService->getLoggableHourTypes());
$this->tpl->assign('ticketHours', $this->timesheetService->getLoggedHoursForTicketByDate($id));
$this->tpl->assign('userHours', $this->timesheetService->getUsersTicketHours($id, session('userdata.id')));
$this->tpl->assign('timesheetsAllHours', $this->timesheetService->getSumLoggedHoursForTicket($id));
$this->tpl->assign('remainingHours', $this->timesheetService->getRemainingHours($ticket));
$this->tpl->assign('userInfo', $this->userService->getUser(session('userdata.id')));
$this->tpl->assign('users', $this->projectService->getUsersWithAccessToProject((int) $ticket->projectId));
$projectData = $this->projectService->getProject($ticket->projectId);
$this->tpl->assign('projectData', $projectData);
$comments = $this->commentService->getComments('ticket', $id);
$this->tpl->assign('numComments', count($comments));
$this->tpl->assign('comments', $comments);
$files = $this->fileService->getFilesByModule('ticket', $id);
$this->tpl->assign('numFiles', count($files));
$this->tpl->assign('files', $files);
$this->tpl->assign('onTheClock', $this->timesheetService->isClocked(session('userdata.id')));
$this->tpl->assign('timesheetValues', [
'kind' => '',
'date' => Carbon::now(session('usersettings.timezone'))->setTimezone('UTC'),
'hours' => '',
'description' => '',
]);
// TODO: Refactor thumbnail generation in file manager
$this->tpl->assign('imgExtensions', ['jpg', 'jpeg', 'png', 'gif', 'psd', 'bmp', 'tif', 'thm', 'yuv']);
$allAssignedprojects = $this->projectService->getProjectsUserHasAccessTo(session('userdata.id'));
$this->tpl->assign('allAssignedprojects', $allAssignedprojects);
// 关联资源BOM/工艺文件/工具清单/文件/wiki
$this->tpl->assign('linkedResources', $this->ticketResourceService->getLinkedResources($id));
$this->tpl->assign('resourceCandidates', $this->ticketResourceService->listCandidates((int) $ticket->projectId));
$response = $this->tpl->displayPartial('tickets.showTicketModal');
$response->headers->set('HX-Trigger', 'ticketUpdate');
return $response;
}
/**
* @throws BindingResolutionException
*/
public function post($params): Response
{
if (! isset($_GET['id'])) {
return $this->tpl->display('errors.error400', responseCode: 400);
}
$tab = '';
$id = (int) ($_GET['id']);
$ticket = $this->ticketService->getTicket($id);
if ($ticket === false) {
return $this->tpl->display('errors.error500', responseCode: 500);
}
// Upload File
if (isset($params['upload'])) {
if ($this->fileService->upload($_FILES, 'ticket', $id, $ticket)) {
$this->tpl->setNotification($this->language->__('notifications.file_upload_success'), 'success');
} else {
$this->tpl->setNotification($this->language->__('notifications.file_upload_error'), 'error');
}
$tab = '#files';
}
// Add or edit a comment
if (isset($params['comment']) === true && isset($params['text']) && $params['text'] != '' && isset($params['edit-comment-helper']) && $params['edit-comment-helper'] !== '') {
if ($this->commentService->editComment($_POST, (int) $params['edit-comment-helper'])) {
$this->tpl->setNotification($this->language->__('notifications.comment_edited_success'), 'success');
} else {
$this->tpl->setNotification($this->language->__('notifications.comment_edit_error'), 'error');
}
$tab = '#comment';
} elseif (isset($params['comment']) === true && isset($params['text']) && $params['text'] != '') {
if ($this->commentService->addComment($_POST, 'ticket', $id, $ticket)) {
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success');
} else {
$this->tpl->setNotification($this->language->__('notifications.comment_create_error'), 'error');
}
$tab = '#comment';
}
// Log time
if (isset($params['saveTimes']) === true) {
try {
$result = $this->timesheetService->logTime($id, $params);
$this->tpl->setNotification($this->language->__('notifications.time_logged_success'), 'success');
} catch (\Exception $e) {
$this->tpl->setNotification($e->getMessage(), 'error');
}
}
// Save Ticket
if (isset($params['saveTicket']) === true || isset($params['saveAndCloseTicket']) === true) {
// $params['projectId'] = $ticket->projectId;
$params['id'] = $id;
// Prepare values, time comes in as 24hours from time input. Service expects time to be in local user format
$params['timeToFinish'] = format(value: $params['timeToFinish'] ?? '', fromFormat: FromFormat::User24hTime)->userTime24toUserTime();
$params['timeFrom'] = format(value: $params['timeFrom'] ?? '', fromFormat: FromFormat::User24hTime)->userTime24toUserTime();
$params['timeTo'] = format(value: $params['timeTo'] ?? '', fromFormat: FromFormat::User24hTime)->userTime24toUserTime();
$result = $this->ticketService->updateTicket($params);
if ($result === true) {
$this->tpl->setNotification($this->language->__('notifications.ticket_saved'), 'success');
} else {
$this->tpl->setNotification($this->language->__($result['msg']), 'error');
}
if (isset($params['saveAndCloseTicket']) === true && $params['saveAndCloseTicket'] == 1) {
$response = Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$id.'?closeModal=1');
$response->headers->set('HX-Trigger', 'ticketUpdate');
return $response;
}
}
$response = Frontcontroller::redirect(BASE_URL.'/tickets/showTicket/'.$id.''.$tab);
$response->headers->set('HX-Trigger', 'ticketUpdate');
return $response;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Leantime\Domain\Tickets\Controllers;
use Illuminate\Http\Request;
use Leantime\Domain\Tickets\Services\TicketResource as TicketResourceService;
use Symfony\Component\HttpFoundation\Response;
/**
* 待办事项 ↔ 资源关联BOM/工艺文件/工具清单/文件/wikiJSON API。
* 每个动作都在 Service 层按 ticket 所属项目自鉴权。
*/
class TicketResourceApi
{
public function __construct(
private TicketResourceService $service,
) {}
/**
* 关联一个资源POST /tickets/resource-api/{ticketId}/link
*/
public function link(int $ticketId, Request $request): Response
{
$type = (string) $request->input('type', '');
$resourceId = (int) $request->input('resourceId', 0);
$result = $this->service->link($ticketId, $type, $resourceId);
return $result['success']
? response()->json(['status' => 'success'])
: response()->json(['status' => 'error', 'message' => $result['message'] ?? '关联失败'], 400);
}
/**
* 解除一个资源关联DELETE /tickets/resource-api/{ticketId}/unlink/{type}/{resourceId}
*/
public function unlink(int $ticketId, string $type, int $resourceId): Response
{
$result = $this->service->unlink($ticketId, $type, $resourceId);
return $result['success']
? response()->json(['status' => 'success'])
: response()->json(['status' => 'error', 'message' => $result['message'] ?? '解除失败'], 400);
}
/**
* 候选资源列表GET /tickets/resource-api/{projectId}/candidates
*/
public function candidates(int $projectId): Response
{
return response()->json(['status' => 'success', 'data' => $this->service->listCandidates($projectId)]);
}
/**
* 已关联资源GET /tickets/resource-api/{ticketId}/links
*/
public function links(int $ticketId): Response
{
return response()->json(['status' => 'success', 'data' => $this->service->getLinkedResources($ticketId)]);
}
}