OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
68
app/Domain/Dashboard/Controllers/Home.php
Normal file
68
app/Domain/Dashboard/Controllers/Home.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Dashboard\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Domain\Setting\Services\Setting;
|
||||
use Leantime\Domain\Widgets\Services\Widgets;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Home extends Controller
|
||||
{
|
||||
private Setting $settingsSvc;
|
||||
|
||||
private Widgets $widgetService;
|
||||
|
||||
public function init(
|
||||
Setting $settingsSvc,
|
||||
Widgets $widgetService
|
||||
): void {
|
||||
|
||||
$this->settingsSvc = $settingsSvc;
|
||||
$this->widgetService = $widgetService;
|
||||
|
||||
session(['lastPage' => BASE_URL.'/dashboard/home']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function get(array $params): Response
|
||||
{
|
||||
// Debug param to reset dashboard
|
||||
if (isset($params['resetDashboard']) === true) {
|
||||
$this->widgetService->resetDashboard(session('userdata.id'));
|
||||
}
|
||||
|
||||
$dashboardGrid = $this->widgetService->getActiveWidgets(session('userdata.id'));
|
||||
$this->tpl->assign('dashboardGrid', $dashboardGrid);
|
||||
|
||||
$completedOnboarding = $this->settingsSvc->onboardingHandler();
|
||||
if ($completedOnboarding instanceof RedirectResponse) {
|
||||
return $completedOnboarding;
|
||||
}
|
||||
|
||||
$this->tpl->assign('completedOnboarding', $completedOnboarding);
|
||||
|
||||
return $this->tpl->display('dashboard.home');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function post($params): Response
|
||||
{
|
||||
// Handle saving dashboard grid layout
|
||||
if (isset($params['action']) && $params['action'] === 'saveGrid' &&
|
||||
isset($params['data']) && $params['data'] !== '') {
|
||||
$this->widgetService->saveGrid($params['data'], session('userdata.id'));
|
||||
|
||||
return new Response;
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/dashboard/home');
|
||||
}
|
||||
}
|
||||
159
app/Domain/Dashboard/Controllers/Show.php
Normal file
159
app/Domain/Dashboard/Controllers/Show.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Dashboard\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Domain\Auth\Models\Roles;
|
||||
use Leantime\Domain\Auth\Services\Auth as AuthService;
|
||||
use Leantime\Domain\Dashboard\Services\Dashboard as DashboardService;
|
||||
use Leantime\Domain\Projects\Services\Projects as ProjectService;
|
||||
use Leantime\Domain\Setting\Services\Setting;
|
||||
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\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Show extends Controller
|
||||
{
|
||||
private ProjectService $projectService;
|
||||
|
||||
private TicketService $ticketService;
|
||||
|
||||
private UserService $userService;
|
||||
|
||||
private TimesheetService $timesheetService;
|
||||
|
||||
private DashboardService $dashboardService;
|
||||
|
||||
private Setting $settingsSvc;
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function init(
|
||||
ProjectService $projectService,
|
||||
TicketService $ticketService,
|
||||
UserService $userService,
|
||||
TimesheetService $timesheetService,
|
||||
DashboardService $dashboardService,
|
||||
Setting $settingsSvc
|
||||
): void {
|
||||
$this->projectService = $projectService;
|
||||
$this->ticketService = $ticketService;
|
||||
$this->userService = $userService;
|
||||
$this->timesheetService = $timesheetService;
|
||||
$this->dashboardService = $dashboardService;
|
||||
$this->settingsSvc = $settingsSvc;
|
||||
|
||||
session(['lastPage' => BASE_URL.'/dashboard/show']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function get(): Response
|
||||
{
|
||||
$currentProjectId = $this->projectService->getCurrentProjectId();
|
||||
if ($currentProjectId === 0) {
|
||||
return FrontcontrollerCore::redirect(BASE_URL.'/dashboard/home');
|
||||
}
|
||||
|
||||
$project = $this->projectService->getProject($currentProjectId);
|
||||
if (isset($project['id']) === false) {
|
||||
return FrontcontrollerCore::redirect(BASE_URL.'/dashboard/home');
|
||||
}
|
||||
|
||||
$projectRedirectFilter = self::dispatch_filter('dashboardRedirect', '/dashboard/show', ['type' => $project['type']]);
|
||||
if ($projectRedirectFilter != '/dashboard/show') {
|
||||
return FrontcontrollerCore::redirect(BASE_URL.$projectRedirectFilter);
|
||||
}
|
||||
|
||||
[$progressSteps, $percentDone] = $this->projectService->getProjectSetupChecklist($currentProjectId);
|
||||
$this->tpl->assign('progressSteps', $progressSteps);
|
||||
$this->tpl->assign('percentDone', $percentDone);
|
||||
|
||||
$project['assignedUsers'] = $this->projectService->getUsersAssignedToProject($currentProjectId);
|
||||
$this->tpl->assign('project', $project);
|
||||
|
||||
$this->tpl->assign('isFavorite', $this->dashboardService->userHasFavoritedProject(session('userdata.id'), $currentProjectId));
|
||||
|
||||
$this->tpl->assign('allUsers', $this->userService->getAll());
|
||||
|
||||
// Project Progress
|
||||
$progress = $this->projectService->getProjectProgress($currentProjectId);
|
||||
$this->tpl->assign('projectProgress', $progress);
|
||||
$this->tpl->assign('currentProjectName', $this->projectService->getProjectName($currentProjectId));
|
||||
|
||||
// Milestones
|
||||
|
||||
$allProjectMilestones = $this->ticketService->getAllMilestones(['sprint' => '', 'type' => 'milestone', 'currentProject' => session('currentProject')]);
|
||||
$this->tpl->assign('milestones', $allProjectMilestones);
|
||||
|
||||
// Delete comment (only confirm success when the auth-checked delete actually ran)
|
||||
if (isset($_GET['delComment']) === true) {
|
||||
if ($this->dashboardService->deleteProjectComment((int) $_GET['delComment'])) {
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_deleted'), 'success', 'projectcomment_deleted');
|
||||
}
|
||||
}
|
||||
|
||||
$this->tpl->assign('delUrlBase', $this->dashboardService->buildDeleteCommentUrlBase());
|
||||
$this->tpl->assign('comments', $this->dashboardService->getProjectCommentsWithReplies($currentProjectId));
|
||||
$this->tpl->assign('numComments', $this->dashboardService->countProjectComments($currentProjectId));
|
||||
|
||||
$completedOnboarding = $this->settingsSvc->onboardingHandler();
|
||||
if ($completedOnboarding instanceof RedirectResponse) {
|
||||
return $completedOnboarding;
|
||||
}
|
||||
|
||||
$this->tpl->assign('completedOnboarding', $completedOnboarding);
|
||||
|
||||
// TICKETS
|
||||
$this->tpl->assign('tickets', $this->ticketService->getLastTickets($currentProjectId));
|
||||
$this->tpl->assign('onTheClock', $this->timesheetService->isClocked(session('userdata.id')));
|
||||
$this->tpl->assign('efforts', $this->ticketService->getEffortLabels());
|
||||
$this->tpl->assign('priorities', $this->ticketService->getPriorityLabels());
|
||||
$this->tpl->assign('types', $this->ticketService->getTicketTypes());
|
||||
$this->tpl->assign('statusLabels', $this->ticketService->getStatusLabels());
|
||||
|
||||
return $this->tpl->display('dashboard.show');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function post($params): Response
|
||||
{
|
||||
|
||||
if (AuthService::userHasRole([Roles::$owner, Roles::$manager, Roles::$editor, Roles::$commenter])) {
|
||||
if (isset($params['quickadd'])) {
|
||||
$result = $this->ticketService->quickAddTicket($params);
|
||||
|
||||
if (isset($result['status'])) {
|
||||
$this->tpl->setNotification($result['message'], $result['status']);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notifications.ticket_saved'), 'success', 'quickticket_created');
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/dashboard/show');
|
||||
}
|
||||
}
|
||||
|
||||
// Manage Post comment
|
||||
if (isset($_POST['comment']) === true) {
|
||||
$currentProjectId = $this->projectService->getCurrentProjectId();
|
||||
$project = $this->projectService->getProject($currentProjectId);
|
||||
|
||||
if ($project && $this->dashboardService->addProjectComment($_POST, $currentProjectId, $project)) {
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_success'), 'success', 'dashboardcomment_created');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notifications.comment_create_error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
return Frontcontroller::redirect(BASE_URL.'/dashboard/show');
|
||||
}
|
||||
}
|
||||
367
app/Domain/Dashboard/Js/dashboardController.js
Normal file
367
app/Domain/Dashboard/Js/dashboardController.js
Normal file
@@ -0,0 +1,367 @@
|
||||
leantime.dashboardController = (function () {
|
||||
|
||||
// Variables (underscore for private variables)
|
||||
|
||||
var chartColors = {};
|
||||
|
||||
if (leantime.theme == "dark") {
|
||||
chartColors = {
|
||||
red: 'rgb(201,48,44)',
|
||||
orange: 'rgb(255, 159, 64)',
|
||||
yellow: 'rgb(255, 205, 86)',
|
||||
green: 'rgb(90,182,90)',
|
||||
blue: 'rgb(54, 162, 235)',
|
||||
purple: 'rgb(153, 102, 255)',
|
||||
grey: 'rgb(56, 56, 56)'
|
||||
};
|
||||
} else {
|
||||
chartColors = {
|
||||
red: 'rgb(201,48,44)',
|
||||
orange: 'rgb(255, 159, 64)',
|
||||
yellow: 'rgb(255, 205, 86)',
|
||||
green: 'rgb(90,182,90)',
|
||||
blue: 'rgb(54, 162, 235)',
|
||||
purple: 'rgb(153, 102, 255)',
|
||||
grey: 'rgb(201, 203, 207)'
|
||||
};
|
||||
}
|
||||
|
||||
var _burndownConfig = '';
|
||||
|
||||
var _progressChart = '';
|
||||
|
||||
//Functions
|
||||
|
||||
var prepareHiddenDueDate = function () {
|
||||
|
||||
var thisFriday = moment().startOf('week').add(5, 'days');
|
||||
jQuery("#dateToFinish").val(thisFriday.format("YYYY-MM-DD"));
|
||||
|
||||
};
|
||||
|
||||
var initProgressChart = function (chartId, complete, incomplete ) {
|
||||
var config = {
|
||||
type: 'doughnut',
|
||||
|
||||
data: {
|
||||
datasets: [{
|
||||
data: [
|
||||
complete,
|
||||
incomplete
|
||||
|
||||
],
|
||||
backgroundColor: [
|
||||
leantime.dashboardController.chartColors.green,
|
||||
leantime.dashboardController.chartColors.grey
|
||||
|
||||
],
|
||||
label: leantime.i18n.__("label.project_done")
|
||||
}],
|
||||
labels: [
|
||||
complete + '% Done',
|
||||
incomplete + '% Open'
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio : false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: ''
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
animateScale: true,
|
||||
animateRotate: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var ctx = document.getElementById(chartId).getContext('2d');
|
||||
_progressChart = new Chart(ctx, config);
|
||||
};
|
||||
|
||||
var initBurndown = function (labels, plannedData, actualData) {
|
||||
|
||||
moment.locale(leantime.i18n.__("language.code"));
|
||||
|
||||
var MONTHS = labels;
|
||||
var config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: leantime.i18n.__("label.ideal"),
|
||||
backgroundColor: leantime.dashboardController.chartColors.blue,
|
||||
borderColor: leantime.dashboardController.chartColors.blue,
|
||||
data: plannedData,
|
||||
fill: false,
|
||||
lineTension: 0,
|
||||
},
|
||||
{
|
||||
label: 'Actual',
|
||||
backgroundColor: leantime.dashboardController.chartColors.red,
|
||||
borderColor: leantime.dashboardController.chartColors.red,
|
||||
data: actualData,
|
||||
fill: false,
|
||||
lineTension: 0,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio : false,
|
||||
|
||||
|
||||
hover: {
|
||||
mode: 'nearest',
|
||||
intersect: true
|
||||
},
|
||||
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Line Chart'
|
||||
},
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: leantime.i18n.__("label.date"),
|
||||
},
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: leantime.i18n.__("label.num_tickets")
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero:true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var ctx2 = document.getElementById('sprintBurndown').getContext('2d');
|
||||
_burndownChart = new Chart(ctx2, config);
|
||||
|
||||
return _burndownChart;
|
||||
|
||||
};
|
||||
|
||||
var initChartButtonClick = function (id, label, plannedData, actualData, chart) {
|
||||
|
||||
jQuery("#" + id).click(
|
||||
function (event) {
|
||||
|
||||
chart.data.datasets[0].data = plannedData;
|
||||
chart.data.datasets[1].data = actualData;
|
||||
chart.options.scales.y.title.text = label;
|
||||
//chart.options.scales.yAxes[0].scaleLabel.labelString = label;
|
||||
jQuery(".chartButtons").removeClass('active');
|
||||
jQuery(this).addClass('active');
|
||||
chart.update();
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
var initBacklogBurndown = function (labels, actualData) {
|
||||
|
||||
moment.locale(leantime.i18n.__("language.code"));
|
||||
|
||||
var MONTHS = labels;
|
||||
var config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
|
||||
{
|
||||
label: leantime.i18n.__("label.done_todos"),
|
||||
backgroundColor: leantime.dashboardController.chartColors.green,
|
||||
borderColor: leantime.dashboardController.chartColors.green,
|
||||
data: actualData.done.data,
|
||||
fill: true,
|
||||
lineTension: 0,
|
||||
pointRadius:0,
|
||||
},
|
||||
{
|
||||
label: leantime.i18n.__("label.progress_todos"),
|
||||
backgroundColor: leantime.dashboardController.chartColors.yellow,
|
||||
borderColor: leantime.dashboardController.chartColors.yellow,
|
||||
data: actualData.progress.data,
|
||||
fill: true,
|
||||
lineTension: 0,
|
||||
pointRadius:0,
|
||||
|
||||
},
|
||||
{
|
||||
label: leantime.i18n.__("label.new_todos"),
|
||||
backgroundColor: leantime.dashboardController.chartColors.red,
|
||||
borderColor: leantime.dashboardController.chartColors.red,
|
||||
data: actualData.open.data,
|
||||
fill: true,
|
||||
lineTension: 0,
|
||||
pointRadius:0,
|
||||
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio : false,
|
||||
hover: {
|
||||
mode: 'nearest',
|
||||
intersect: true
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
pointStyle: "line",
|
||||
radius:"0"
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltips: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
text: 'Line Chart'
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: leantime.i18n.__("label.date"),
|
||||
|
||||
},
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day'
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: leantime.i18n.__("label.num_tickets")
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero:true
|
||||
},
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var ctx2 = document.getElementById('backlogBurndown').getContext('2d');
|
||||
_burndownChart = new Chart(ctx2, config);
|
||||
|
||||
return _burndownChart;
|
||||
|
||||
};
|
||||
|
||||
var initBacklogChartButtonClick = function (id, actualData, label, chart) {
|
||||
|
||||
jQuery("#" + id).click(
|
||||
function (event) {
|
||||
|
||||
chart.data.datasets[0].data = actualData.done.data;
|
||||
|
||||
chart.data.datasets[1].data = actualData.progress.data;
|
||||
chart.data.datasets[2].data = actualData.open.data;
|
||||
|
||||
|
||||
chart.options.scales.y.title.text = label;
|
||||
jQuery(".backlogChartButtons").removeClass('active');
|
||||
jQuery(this).addClass('active');
|
||||
chart.update();
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
var initDueDateTimePickers = function () {
|
||||
jQuery(document).ready(function () {
|
||||
|
||||
|
||||
jQuery(".duedates").datepicker(
|
||||
{
|
||||
dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"),
|
||||
dayNames: leantime.i18n.__("language.dayNames").split(","),
|
||||
dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","),
|
||||
dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","),
|
||||
monthNames: leantime.i18n.__("language.monthNames").split(","),
|
||||
currentText: leantime.i18n.__("language.currentText"),
|
||||
closeText: leantime.i18n.__("language.closeText"),
|
||||
buttonText: leantime.i18n.__("language.buttonText"),
|
||||
isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0,
|
||||
nextText: leantime.i18n.__("language.nextText"),
|
||||
prevText: leantime.i18n.__("language.prevText"),
|
||||
weekHeader: leantime.i18n.__("language.weekHeader"),
|
||||
onClose: function (date) {
|
||||
|
||||
var newDate = "";
|
||||
|
||||
if (date == "") {
|
||||
jQuery(this).val(leantime.i18n.__("text.anytime"));
|
||||
}
|
||||
|
||||
var dateTime = moment(date, leantime.i18n.__("language.momentJSDate")).format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
var id = jQuery(this).attr("data-id");
|
||||
newDate = dateTime;
|
||||
|
||||
leantime.ticketsRepository.updateDueDates(id, newDate, function () {
|
||||
jQuery.growl({message: leantime.i18n.__("short_notifications.duedate_updated")});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Make public what you want to have public, everything else is private
|
||||
return {
|
||||
chartColors: chartColors,
|
||||
initBurndown: initBurndown,
|
||||
initChartButtonClick: initChartButtonClick,
|
||||
initBacklogBurndown:initBacklogBurndown,
|
||||
initBacklogChartButtonClick:initBacklogChartButtonClick,
|
||||
initProgressChart:initProgressChart,
|
||||
prepareHiddenDueDate:prepareHiddenDueDate,
|
||||
initDueDateTimePickers:initDueDateTimePickers
|
||||
};
|
||||
})();
|
||||
18
app/Domain/Dashboard/Repositories/Dashboard.php
Normal file
18
app/Domain/Dashboard/Repositories/Dashboard.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Dashboard\Repositories;
|
||||
|
||||
use Leantime\Core\Db\Db as DbCore;
|
||||
|
||||
class Dashboard
|
||||
{
|
||||
public ?DbCore $db;
|
||||
|
||||
/**
|
||||
* __construct - neu db connection
|
||||
*/
|
||||
public function __construct(DbCore $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
}
|
||||
140
app/Domain/Dashboard/Services/Dashboard.php
Normal file
140
app/Domain/Dashboard/Services/Dashboard.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Dashboard\Services;
|
||||
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
use Leantime\Domain\Comments\Services\Comments as CommentService;
|
||||
use Leantime\Domain\Reactions\Models\Reactions;
|
||||
use Leantime\Domain\Reactions\Services\Reactions as ReactionService;
|
||||
|
||||
/**
|
||||
* Dashboard service - business logic backing the project dashboard view.
|
||||
*
|
||||
* Wraps the comment and reaction data assembly the dashboard needs so the
|
||||
* controllers stay thin (param-read -> service call -> assign -> Response).
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Dashboard
|
||||
{
|
||||
/**
|
||||
* @param CommentService $commentService Comment business logic (authorization-aware deletes)
|
||||
* @param CommentRepository $commentRepository Comment data access (replies, counts)
|
||||
* @param ReactionService $reactionsService Reaction business logic
|
||||
*/
|
||||
public function __construct(
|
||||
private CommentService $commentService,
|
||||
private CommentRepository $commentRepository,
|
||||
private ReactionService $reactionsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the top-level comments for a project with their replies attached.
|
||||
*
|
||||
* Mirrors the legacy dashboard behavior: top-level comments (parent 0)
|
||||
* each enriched with a 'replies' key holding their direct replies.
|
||||
*
|
||||
* @param int $projectId The project to load comments for
|
||||
* @return array<int, array<string, mixed>> Top-level comments, each with a 'replies' array
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getProjectCommentsWithReplies(int $projectId): array
|
||||
{
|
||||
if ($projectId <= 0) {
|
||||
throw new \InvalidArgumentException('A valid project id is required to load comments.');
|
||||
}
|
||||
|
||||
$comments = $this->commentService->getComments('project', $projectId, 0);
|
||||
|
||||
if (! is_array($comments)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(function ($comment) {
|
||||
$comment['replies'] = $this->commentRepository->getReplies($comment['id']);
|
||||
|
||||
return $comment;
|
||||
}, $comments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts all comments attached to a project.
|
||||
*
|
||||
* @param int $projectId The project to count comments for
|
||||
* @return int The number of comments
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function countProjectComments(int $projectId): int
|
||||
{
|
||||
if ($projectId <= 0) {
|
||||
throw new \InvalidArgumentException('A valid project id is required to count comments.');
|
||||
}
|
||||
|
||||
return (int) $this->commentRepository->countComments('project', $projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a project comment.
|
||||
*
|
||||
* Delegates to the comment service so its author/manager authorization
|
||||
* check is enforced (the legacy dashboard deleted directly with no check).
|
||||
*
|
||||
* @param int $commentId The comment to delete
|
||||
* @return bool True if the comment was deleted
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function deleteProjectComment(int $commentId): bool
|
||||
{
|
||||
return $this->commentService->deleteComment($commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a comment to a project.
|
||||
*
|
||||
* @param array<string, mixed> $values The submitted comment values
|
||||
* @param int $projectId The project the comment belongs to
|
||||
* @param array<string, mixed> $project The loaded project entity
|
||||
* @return bool True if the comment was created
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function addProjectComment(array $values, int $projectId, array $project): bool
|
||||
{
|
||||
return $this->commentService->addComment($values, 'project', $projectId, $project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a user has favorited a project.
|
||||
*
|
||||
* @param int $userId The user to check
|
||||
* @param int $projectId The project to check
|
||||
* @return bool True if the user has a favorite reaction on the project
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function userHasFavoritedProject(int $userId, int $projectId): bool
|
||||
{
|
||||
$userReaction = $this->reactionsService->getUserReactions($userId, 'project', $projectId, Reactions::$favorite);
|
||||
|
||||
return $userReaction && is_array($userReaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the base URL used to delete a dashboard comment.
|
||||
*
|
||||
* Derives scheme/host/path from the current request URL and appends the
|
||||
* delComment query parameter, leaving the caller to suffix the comment id.
|
||||
*
|
||||
* @return string The delete-comment URL base (ends with 'delComment=')
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function buildDeleteCommentUrlBase(): string
|
||||
{
|
||||
// Current URL up to (but excluding) any existing query string, plus the delComment flag.
|
||||
return strtok(CURRENT_URL, '?').'?delComment=';
|
||||
}
|
||||
}
|
||||
54
app/Domain/Dashboard/Templates/home.blade.php
Normal file
54
app/Domain/Dashboard/Templates/home.blade.php
Normal file
@@ -0,0 +1,54 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="maincontent" id="gridBoard" style="margin-top:0px; opacity:0;">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="grid-stack">
|
||||
|
||||
@foreach($dashboardGrid as $widget)
|
||||
|
||||
<x-widgets::moveableWidget
|
||||
gs-x="{{ $widget->gridX }}"
|
||||
gs-y="{{ $widget->gridY }}"
|
||||
gs-h="{{ $widget->gridHeight }}"
|
||||
gs-w="{{ $widget->gridWidth }}"
|
||||
gs-min-w="{{ $widget->gridMinWidth }}"
|
||||
gs-min-h="{{ $widget->gridMinHeight }}"
|
||||
isNew="{{ isset($widget->isNew) ? 'true' : 'false' }}"
|
||||
background="{{ $widget->widgetBackground }}"
|
||||
noTitle="{{ $widget->noTitle }}"
|
||||
name="{{ $widget->name }}"
|
||||
:fixed="(empty($widget->fixed) ? false : true )"
|
||||
alwaysVisible="{{ $widget->alwaysVisible }}"
|
||||
id="widget_wrapper_{{ $widget->id }}"
|
||||
>
|
||||
<div hx-get="{{$widget->widgetUrl }}"
|
||||
hx-trigger="revealed"
|
||||
id="{{ $widget->id }}"
|
||||
class="tw-h-full"
|
||||
hx-swap="innerHTML">
|
||||
<x-global::loadingText type="{{ $widget->widgetLoadingIndicator }}" count="1" includeHeadline="true" />
|
||||
</div>
|
||||
</x-widgets::moveableWidget>
|
||||
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
|
||||
leantime.widgetController.initGrid();
|
||||
|
||||
@php(session(["usersettings.modals.homeDashboardTour" => 1]))
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,11 @@
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div class="btn-group pull-left" style="margin-right:5px;">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown"><?=$tpl->__("links.new_with_icon") ?> <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#/tickets/newTicket">Add Todo</a></li>
|
||||
<li><a href="#/tickets/editMilestone">Add Milestone</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
594
app/Domain/Dashboard/Templates/show.blade.php
Normal file
594
app/Domain/Dashboard/Templates/show.blade.php
Normal file
@@ -0,0 +1,594 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
<x-global::pageheader :icon="'fa fa-gauge-high'">
|
||||
@if (count($allUsers) == 1)
|
||||
<a href="#/users/newUser" class="headerCTA">
|
||||
<i class="fa fa-users"></i>
|
||||
<span class="tw-text-[14px] tw-leading-[25px]">
|
||||
{{ __('links.dont_do_it_alone') }}
|
||||
</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<h5>{{ session("currentProjectClient") }}</h5>
|
||||
<h1>{!! __('headlines.project_dashboard') !!}</h1>
|
||||
</x-global::pageheader>
|
||||
|
||||
<div class="maincontent">
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-8">
|
||||
|
||||
<div class="maincontentinner tw-z-20">
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$admin))
|
||||
<div class="pull-right dropdownWrapper">
|
||||
<a
|
||||
class="dropdown-toggle btn round-button"
|
||||
data-toggle="dropdown"
|
||||
data-tippy-content="{{ __('label.edit_project') }}"
|
||||
href="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}"
|
||||
><i class="fa fa-ellipsis"></i></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-item">
|
||||
<a
|
||||
href="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}"
|
||||
|
||||
><i class="fa fa-edit"></i> Edit Project</a>
|
||||
</li>
|
||||
<li class="dropdown-item">
|
||||
<a
|
||||
href="{{ BASE_URL }}/projects/delProject/{{ $project['id'] }}"
|
||||
class="delete"
|
||||
|
||||
><i class="fa fa-trash"></i> Delete Project</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pull-right dropdownWrapper tw-mr-[5px]">
|
||||
<a
|
||||
class="dropdown-toggle btn round-button"
|
||||
data-toggle="dropdown"
|
||||
data-tippy-content="{{ __('label.copy_url_tooltip') }}"
|
||||
href="{{ BASE_URL }}/projects/changeCurrentProject/{{ $project['id'] }}"
|
||||
><i class="fa fa-link"></i></a>
|
||||
<div class="dropdown-menu padding-md">
|
||||
<x-global::forms.text-input id="projectUrl" value="{{ BASE_URL }}/projects/changeCurrentProject/{{ $project['id'] }}" />
|
||||
<x-global::forms.button contentRole="primary" onclick="leantime.snippets.copyUrl('projectUrl')">{{ __('links.copy_url') }}</x-global::forms.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
id="favoriteProject"
|
||||
class="btn pull-right margin-right {{ $isFavorite ? 'isFavorite' : ''}} tw-mr-[5px] round-button"
|
||||
data-tippy-content="{{ __('label.favorite_tooltip') }}"
|
||||
><i class="{{ $isFavorite ? 'fa-solid' : 'fa-regular' }} fa-star"></i></a>
|
||||
|
||||
|
||||
|
||||
<h3>{{ session("currentProjectClient") }}</h3>
|
||||
|
||||
<h1 class="articleHeadline">{{ $currentProjectName }}</h1>
|
||||
|
||||
<br/>
|
||||
|
||||
@include('projects::partials.checklist', [
|
||||
'progressSteps' => $progressSteps,
|
||||
'percentDone' => $percentDone
|
||||
])
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<strong>{{ __('label.background') }}</strong><br/>
|
||||
<div class="readMoreBox">
|
||||
<div class="tiptap-content kanbanContent closed tw-max-h-[200px] readMoreContent tw-pb-[30px]" id="projectDescription">
|
||||
{!! $tpl->escapeMinimal($project['details']) !!}
|
||||
</div>
|
||||
|
||||
<div class="center readMoreToggle" style="display:none;">
|
||||
<a href="javascript:void(0)" id="descriptionReadMoreToggle">{{ __('label.read_more') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="maincontentinner tw-z-10 latest-todos">
|
||||
<x-global::forms.button tag="a" link="#/tickets/newTicket" contentRole="link" class="action-link pull-right" style="margin-top:-7px;"><i class="fa fa-plus"></i> Create To-Do</x-global::forms.button>
|
||||
<h5 class="subtitle">{{ __('headlines.latest_todos') }}</h5>
|
||||
<br/>
|
||||
<ul class="sortableTicketList">
|
||||
@if (count($tickets) == 0)
|
||||
<em>Nothing to see here. Move on.</em><br/><br/>
|
||||
@endif
|
||||
|
||||
@foreach($tickets as $row)
|
||||
<li class="ui-state-default" id="ticket_{!! $row['id'] !!}">
|
||||
<div class="ticketBox fixed priority-border-{!! $row['priority'] !!}" data-val="{!! $row['id'] !!}">
|
||||
<div class="row">
|
||||
<div class="col-md-12 timerContainer tw-py-[5px] tw-px-[15px]" id="timerContainer-{!! $row['id'] !!}">
|
||||
@if($row['dependingTicketId'] > 0)
|
||||
<a href="#/tickets/showTicket/{{ $row['dependingTicketId'] }}">
|
||||
{{ $row['parentHeadline'] }}
|
||||
</a>
|
||||
//
|
||||
@endif
|
||||
|
||||
<a href="#/tickets/showTicket/{{ $row['id'] }}">
|
||||
<strong>{{ $row['headline'] }}</strong>
|
||||
</a>
|
||||
|
||||
@include("tickets::partials.ticketsubmenu", ["ticket" => $row,"onTheClock" => $onTheClock])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 tw-px-[15px] tw-py-0">
|
||||
|
||||
<i class="fa-solid fa-business-time infoIcon" data-tippy-content=" {{ __("label.due") }}"></i>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
title="{{ __('label.due') }}"
|
||||
value="{{ format($row['dateToFinish'])->date(__('text.anytime')) }}"
|
||||
class="duedates secretInput"
|
||||
data-id="{{ $row['id'] }}"
|
||||
name="date"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-8 tw-mt-[3px]">
|
||||
<div class="right">
|
||||
<div class="dropdown ticketDropdown effortDropdown show">
|
||||
<a
|
||||
class="dropdown-toggle f-left label-default effort"
|
||||
href="javascript:void(0);"
|
||||
role="button"
|
||||
id="effortDropdownMenuLink{{ $row['id'] }}"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span class="text">
|
||||
{{ $row['storypoints'] != '' && $row['storypoints'] > 0
|
||||
? ($efforts[''.$row['storypoints'].''] ?? $row['storypoints'])
|
||||
: __('label.story_points_unkown')
|
||||
}}
|
||||
</span> <i class="fa fa-caret-down" aria-hidden="true"></i></a>
|
||||
|
||||
<ul class="dropdown-menu" aria-labelledby="effortDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{{ __('dropdown.how_big_todo') }}</li>
|
||||
@foreach ($efforts as $effortKey => $effortValue)
|
||||
<li class="dropdown-item">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
data-value="{{ $row['id'] }}_{{ $effortKey}}"
|
||||
id="ticketEffortChange_{{ $row['id'] . $effortKey }}"
|
||||
>{{ $effortValue }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown ticketDropdown milestoneDropdown colorized show">
|
||||
<a
|
||||
style="background-color:{{ __($row['milestoneColor']) }}"
|
||||
class="dropdown-toggle f-left label-default milestone"
|
||||
href="javascript:void(0);"
|
||||
role="button"
|
||||
id="milestoneDropdownMenuLink{{ $row['id'] }}"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span class="text">
|
||||
{{ $row['milestoneid'] != '' && $row['milestoneid'] != 0
|
||||
? $row['milestoneHeadline']
|
||||
: __('label.no_milestone')
|
||||
}}
|
||||
</span> <i class="fa fa-caret-down" aria-hidden="true"></i></a>
|
||||
|
||||
<ul class="dropdown-menu" aria-labeledby="milestoneDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{{ __('dropdown.choose_milestone') }}</li>
|
||||
<li class="dropdown-item">
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
data-label="{{ __('label.no_milestone') }}"
|
||||
data-value="{{ $row['id'] }}_0_#b0b0b0"
|
||||
class="tw-bg-[#b0b0b0]"
|
||||
>{{ __('label.no_milestone') }}</a>
|
||||
</li>
|
||||
@foreach ($milestones as $milestone)
|
||||
<li class="dropdown-item">
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
data-label="{{ $milestone->headline }}"
|
||||
data-value="{{ $row['id'] }}_{!! $milestone->id !!}_{{ $milestone->tags }}"
|
||||
id="ticketMilestoneChange_{{ $row['id'] . $milestone->id }}"
|
||||
style="background-color:{{ $milestone->tags }}"
|
||||
>{{ $milestone->headline }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show">
|
||||
<a
|
||||
class="dropdown-toggle f-left status {!! $statusLabels[$row['status']]['class'] !!}"
|
||||
href="javascript:void(0);"
|
||||
role="button"
|
||||
id="statusDropdownMenuLink{{ $row['id'] }}"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
><span class="text">{!! $statusLabels[$row['status']]['name'] !!}</span> <i class="fa fa-caret-down" aria-hidden="true"></i></a>
|
||||
|
||||
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{!! $row['id'] !!}">
|
||||
<li class="nav-header border">{{ __('dropdown.choose_status') }}</li>
|
||||
@foreach ($statusLabels as $key => $label)
|
||||
<li class="dropdown-item">
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
class="{!! $label['class'] !!}"
|
||||
data-label="{{ $label['name'] }}"
|
||||
data-value="{{ $row['id'] }}_{{ $key }}_{!! $label['class'] !!}"
|
||||
id="ticketStatusChange{{ $row['id'] . $key }}"
|
||||
>{{ $label['name'] }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="maincontentinner team-container">
|
||||
@dispatchEvent('teamBoxBeginning', ['project' => $project])
|
||||
|
||||
<h5 class="subtitle">{{ __('tabs.team') }}</h5>
|
||||
|
||||
<div class="row teamBox">
|
||||
@foreach ($project['assignedUsers'] as $userId => $assignedUser)
|
||||
<div class="col-md-3">
|
||||
<x-users::profile-box :user="$assignedUser">
|
||||
@spaceless
|
||||
@php $hasName = $assignedUser['firstname'] != '' || $assignedUser['lastname'] != ''; @endphp
|
||||
|
||||
@if ($hasName)
|
||||
{{ sprintf(
|
||||
__('text.full_name'),
|
||||
$assignedUser['firstname'],
|
||||
$assignedUser['lastname'],
|
||||
) }}
|
||||
@else
|
||||
{{ $assignedUser['username'] }}
|
||||
@endif
|
||||
|
||||
<br />
|
||||
<small>{{ $hasName ? $assignedUser['jobTitle'] : __('label.invited') }}</small>
|
||||
|
||||
@if ($hasName)
|
||||
@dispatchEvent('usercardBottom', ['user' => $assignedUser, 'project' => $project])
|
||||
@endif
|
||||
@endspaceless
|
||||
</x-users::profile-box>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$manager))
|
||||
<div class="col-md-3">
|
||||
<x-users::profile-box>
|
||||
<a href="#/users/newUser?preSelectProjectId={{ $project['id'] }}">
|
||||
{{ __('links.invite_user') }}
|
||||
</a><br/>
|
||||
</x-users::profile-box>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="maincontentinner project-updates">
|
||||
<div class="pull-right">
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<x-global::forms.button
|
||||
tag="a"
|
||||
link="javascript:void(0);"
|
||||
onclick="leantime.commentsController.toggleCommentBoxes(0);jQuery('.noCommentsMessage').toggle();"
|
||||
id="mainToggler"
|
||||
contentRole="link"
|
||||
class="action-link"
|
||||
style="margin-top:-7px;"
|
||||
><span class="fa fa-plus"></span> {{ __('links.add_new_report') }}</x-global::forms.button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h5 class="subtitle">{{ __('subtitles.project_updates') }}</h5>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/dashboard/show">
|
||||
<input type="hidden" name="comment" value="1" />
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div id="comment0" class="commentBox tw-hidden">
|
||||
<label for="projectStatus tw-inline">{{ __('label.project_status_is') }}</label>
|
||||
|
||||
<select name="status" id="projectStatus" class="tw-ml-0 tw-mb-[10px]">
|
||||
<option value="green">{{ __('label.project_status_green') }}</option>
|
||||
<option value="yellow">{{ __('label.project_status_yellow') }}</option>
|
||||
<option value="red">{{ __('label.project_status_red') }}</option>
|
||||
</select>
|
||||
|
||||
<div class="commentReply">
|
||||
<textarea rows="5" cols="50" class="tiptapSimple tw-w-full" name="text"></textarea>
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ __('buttons.save') }}"
|
||||
name="comment"
|
||||
class="btn btn-primary btn-success tw-ml-0"
|
||||
/>
|
||||
<x-global::forms.button
|
||||
tag="a"
|
||||
link="javascript:void(0);"
|
||||
onclick="leantime.commentsController.toggleCommentBoxes(-1);jQuery('.noCommentsMessage').toggle();"
|
||||
class="tw-leading-[50px]"
|
||||
contentRole="secondary"
|
||||
>{{ __('links.cancel') }}</x-global::forms.button>
|
||||
<input type="hidden" name="comment" value="1"/>
|
||||
<input type="hidden" name="father" id="father" value="0"/>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div id="comments">
|
||||
@foreach ($comments as $row)
|
||||
@if ($loop->iteration == 3)
|
||||
<a href="javascript:void(0);" onclick="jQuery('.readMore').toggle('fast')">
|
||||
{{ __('links.read_more') }}
|
||||
</a>
|
||||
<div class="readMore tw-hidden tw-mt-[20px]">
|
||||
@endif
|
||||
<div class="clearall">
|
||||
<div>
|
||||
<div class="commentContent statusUpdate commentStatus-{{ $row['status'] }}">
|
||||
<strong class="fancyLink">
|
||||
{{ sprintf(
|
||||
__('text.report_written_on'),
|
||||
format($row['date'])->date(),
|
||||
format($row['date'])->time()
|
||||
) }}
|
||||
</strong>
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
<div class="inlineDropDownContainer tw-float-right tw-ml-[10px]">
|
||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-ellipsis-v"></i>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
@if ($row['userId'] == session("userdata.id"))
|
||||
<li>
|
||||
<a href="{!! $delUrlBase . $row['id'] !!}" class="deleteComment">
|
||||
<span class="fa fa-trash"></span> {{ __('links.delete') }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@isset($ticket->id)
|
||||
<li>
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
onclick="leantime.ticketsController.addCommentTimesheetContent({!! $row['id'] !!}, {!! $ticket->id !!})"
|
||||
>{{ __('links.add_to_timesheets') }}</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="text" id="commentText-{{ $row['id'] }}">{!! $tpl->escapeMinimal($row['text']) !!}</div>
|
||||
</div>
|
||||
|
||||
<div class="commentLinks">
|
||||
<small class="right">
|
||||
{!! sprintf(
|
||||
__('text.written_on_by'),
|
||||
format($row['date'])->date(),
|
||||
format($row['date'])->time(),
|
||||
$tpl->escape($row['firstname']),
|
||||
$tpl->escape($row['lastname'])
|
||||
) !!}
|
||||
</small>
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$commenter))
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
onclick="leantime.commentsController.toggleCommentBoxes({!! $row['id'] !!});"
|
||||
><span class="fa fa-reply"></span> {{ __('links.reply') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="replies">
|
||||
@if ($row['replies'])
|
||||
@foreach ($row['replies'] as $comment)
|
||||
<x-comments::reply :comment="$comment" :iteration="$loop->iteration" />
|
||||
@endforeach
|
||||
@endif
|
||||
<x-comments::input :commentId="$row['id']" :user="session('userdata')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if (count($comments) >= 3)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (count($comments) == 0)
|
||||
<div style="padding-left:0px; clear:both;" class="noCommentsMessage">
|
||||
{{ __('text.no_updates') }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="clearall"></div>
|
||||
</form>
|
||||
<div class="clearall"></div>
|
||||
</div>
|
||||
|
||||
<div class="maincontentinner project-progress">
|
||||
<div class="row" id="projectProgressContainer">
|
||||
<div class="col-md-12">
|
||||
<h5 class="subtitle">{{ __('subtitles.project_progress') }}</h5>
|
||||
|
||||
<div id="canvas-holder" class="tw-w-full tw-h-[250px]">
|
||||
<canvas id="chart-area"></canvas>
|
||||
</div>
|
||||
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="milestoneProgressContainer">
|
||||
<div class="col-md-12">
|
||||
<h5 class="subtitle">{{ __('headline.milestones') }}</h5>
|
||||
<ul class="sortableTicketList">
|
||||
@if (count($milestones) == 0)
|
||||
<div class="center">
|
||||
<br/>
|
||||
<h4>{{ __('headlines.no_milestones') }}</h4>
|
||||
{{ __('text.milestones_help_organize_projects') }}
|
||||
<br/><br/>
|
||||
<a href="{{ BASE_URL }}/tickets/roadmap">{!! __('links.goto_milestones') !!}</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@foreach($milestones as $row)
|
||||
@if ($row->percentDone >= 100 && (new \DateTime($row->editTo) < new \DateTime()))
|
||||
@break
|
||||
@endif
|
||||
|
||||
<li class="ui-state-default" id="milestone_{!! $row->id !!}">
|
||||
|
||||
<div hx-trigger="load"
|
||||
hx-indicator=".htmx-indicator"
|
||||
hx-get="<?= BASE_URL ?>/hx/tickets/milestones/showCard?milestoneId=<?= $row->id ?>">
|
||||
<div class="htmx-indicator">
|
||||
<?= $tpl->__('label.loading_milestone') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type='text/javascript'>
|
||||
if (window.leantime && window.leantime.tiptapController) {
|
||||
leantime.tiptapController.initSimpleEditor();
|
||||
}
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@once @push('scripts')
|
||||
<script>
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
|
||||
jQuery('#descriptionReadMoreToggle').click(function() {
|
||||
|
||||
if (jQuery("#projectDescription").hasClass("closed")) {
|
||||
jQuery("#projectDescription").css("max-height", "100%");
|
||||
jQuery("#projectDescription").removeClass("closed");
|
||||
jQuery("#projectDescription").removeClass("kanbanContent");
|
||||
jQuery('#descriptionReadMoreToggle').text("{{ __('label.read_less') }}");
|
||||
} else {
|
||||
jQuery("#projectDescription").css("max-height", "200px");
|
||||
jQuery("#projectDescription").addClass("closed");
|
||||
jQuery("#projectDescription").addClass("kanbanContent");
|
||||
jQuery('#descriptionReadMoreToggle').text("{{ __('label.read_more') }}");
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(".readMoreBox").each(function() {
|
||||
if (jQuery(this).find(".readMoreContent").height() >= 169) {
|
||||
|
||||
jQuery(this).find(".readMoreToggle").show();
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(document).on('click', '.progressWrapper .dropdown-menu', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.dashboardController.prepareHiddenDueDate();
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initMilestoneDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
leantime.usersController.initUserEditModal();
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
leantime.dashboardController.initProgressChart(
|
||||
"chart-area",
|
||||
{!! round($projectProgress['percent']) !!},
|
||||
{!! round(100 - $projectProgress['percent']) !!}
|
||||
);
|
||||
|
||||
jQuery("#favoriteProject").click(function() {
|
||||
if (jQuery("#favoriteProject").hasClass("isFavorite")) {
|
||||
leantime.reactionsController.removeReaction(
|
||||
'project',
|
||||
{!! $project['id'] !!},
|
||||
'favorite',
|
||||
function() {
|
||||
jQuery("#favoriteProject").find("i").removeClass("fa-solid").addClass("fa-regular");
|
||||
jQuery("#favoriteProject").removeClass("isFavorite");
|
||||
}
|
||||
);
|
||||
} else {
|
||||
leantime.reactionsController.addReactions(
|
||||
'project',
|
||||
{!! $project['id'] !!},
|
||||
'favorite',
|
||||
function() {
|
||||
jQuery("#favoriteProject").find("i").removeClass("fa-regular").addClass("fa-solid");
|
||||
jQuery("#favoriteProject").addClass("isFavorite");
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
|
||||
|
||||
@php(session(["usersettings.modals.projectDashboardTour" => 1]));
|
||||
});
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user