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,267 @@
<div class="row">
<div class="col-md-12">
<div class="row marginBottom">
<div class="col-md-12">
<!-- Type -->
<div class="form-group">
<label class="control-label">{!! __('label.todo_type') !!}</label>
<div class="">
<select id='type' name='type' class="span11">
@foreach ($ticketTypes as $types)
<option value="{{ strtolower($types) }}"
@if (strtolower($types) == strtolower($ticket->type ?? '')) selected='selected' @endif
>{!! __('label.' . strtolower($types)) !!}</option>
@endforeach
</select><br/>
</div>
</div>
</div>
</div>
<div class="row marginBottom">
<div class="col-md-12">
<h5 class="accordionTitle" id="accordion_link_tickets-organization" style="padding-bottom:15px; font-size:var(--font-size-l)">
<a href="javascript:void(0)"
class="accordion-toggle"
id="accordion_toggle_tickets-organization"
onclick="leantime.snippets.accordionToggle('tickets-organization');">
<i class="fa fa-angle-down"></i>
<span class="fa fa-folder-open"></span>
{!! __('subtitles.organization') !!}
</a>
</h5>
<div class="simpleAccordionContainer" id="accordion_content-tickets-organization" style="padding-left:0">
<!-- Project -->
<div class="form-group">
<label class="control-label">{!! __('label.project') !!}</label>
<select name="projectId" class="tw-w-full">
@foreach ($allAssignedprojects as $project)
<option value="{{ $project['id'] }}"
@if ($ticket->projectId == $project['id'])
selected
@elseif (session('currentProject') == $project['id'])
selected
@endif
>{{ $tpl->escape($project['name']) }}</option>
@endforeach
</select>
</div>
<!-- Milestones -->
<div class="form-group">
<label class="control-label">{!! __('label.milestone') !!}</label>
<div class="">
<div class="form-group">
<select name="milestoneid" class="span11" >
<option value="">{!! __('label.not_assigned_to_milestone') !!}</option>
@foreach ($milestones as $milestoneRow)
<option value="{{ $milestoneRow->id }}"
@if ($ticket->milestoneid == $milestoneRow->id) selected='selected' @endif
>{{ $tpl->escape($milestoneRow->headline) }}</option>
@endforeach
</select>
</div>
</div>
</div>
<!-- Sprint -->
<div class="form-group">
<label class="control-label">{!! __('label.sprint') !!}</label>
<div class="">
<select id="sprint-select" class="span11" name="sprint"
data-placeholder="{{ $ticket->sprint }}">
<option value="">{!! __('label.backlog') !!}</option>
@if ($sprints)
@foreach ($sprints as $sprintRow)
<option value="{{ $sprintRow->id }}"
@if ($ticket->sprint == $sprintRow->id) selected='selected' @endif
>{{ $sprintRow->name }}</option>
@endforeach
@endif
</select>
</div>
</div>
<!-- Related -->
<div class="form-group">
<label class="control-label">{!! __('label.related_to') !!}</label>
<div class="">
<div class="form-group">
<select name="dependingTicketId" class="span11" >
<option value="">{!! __('label.not_related') !!}</option>
@if (is_array($ticketParents))
@foreach ($ticketParents as $ticketRow)
<option value="{{ $ticketRow->id }}"
@if ($ticket->dependingTicketId == $ticketRow->id) selected='selected' @endif
>{{ $tpl->escape($ticketRow->headline) }}</option>
@endforeach
@endif
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row marginBottom">
<div class="col-md-12">
<h5 class="accordionTitle" id="accordion_link_tickets-dates" style="padding-bottom:15px; font-size:var(--font-size-l)">
<a href="javascript:void(0)"
class="accordion-toggle"
id="accordion_toggle_tickets-dates"
onclick="leantime.snippets.accordionToggle('tickets-dates');">
<i class="fa fa-angle-down"></i>
<span class="fa fa-calendar"></span>
{!! __('subtitles.dates') !!}
</a>
</h5>
<div class="simpleAccordionContainer" id="accordion_content-tickets-dates" style="padding-left:0">
<div class="form-group">
<label class=" control-label">{!! __('label.working_date_from') !!}</label>
<div class="">
<input type="text" class="editFrom" style="width:100px;" name="editFrom" autocomplete="off"
value="{{ format($ticket->editFrom)->date() }}" placeholder="{{ __('language.dateformat') }}"/>
<input type="time" class="timepicker" style="width:120px;" id="timeFrom" autocomplete="off"
value="{{ format($ticket->editFrom)->time24() }}"
name="timeFrom"/>
</div>
</div>
<div class="form-group">
<label class=" control-label">{!! __('label.working_date_to') !!}</label>
<div class="">
<input type="text" class="editTo" style="width:100px;" name="editTo" autocomplete="off"
value="{{ format($ticket->editTo)->date() }}" placeholder="{{ __('language.dateformat') }}"/>
<input type="time" class="timepicker" style="width:120px;" id="timeTo" autocomplete="off"
value="{{ format($ticket->editTo)->time24() }}"
name="timeTo"/>
</div>
</div>
</div>
</div>
</div>
<div class="row marginBottom">
<div class="col-md-12">
<h5 class="accordionTitle" id="accordion_link_tickets-timetracking" style="padding-bottom:15px; font-size:var(--font-size-l)">
<a href="javascript:void(0)"
class="accordion-toggle"
id="accordion_toggle_tickets-timetracking"
onclick="leantime.snippets.accordionToggle('tickets-timetracking');">
<i class="fa fa-angle-down"></i>
<span class="fa-regular fa-clock"></span>
{!! __('subtitle.time_tracking') !!}
</a>
</h5>
<div class="simpleAccordionContainer" id="accordion_content-tickets-timetracking" style="padding-left:0">
<div class="form-group">
<label class=" control-label">{!! __('label.planned_hours') !!}</label>
<div class="">
<x-global::forms.text-input value="{{ $ticket->planHours }}" name="planHours" style="width:90px;" />
</div>
</div>
<div class="form-group">
<label class=" control-label">{!! __('label.estimated_hours_remaining') !!}</label>
<div class="">
<x-global::forms.text-input value="{{ $ticket->hourRemaining }}" name="hourRemaining" style="width:90px;" />
<a href="javascript:void(0)" class="infoToolTip" data-placement="left" data-toggle="tooltip" data-tippy-content="{{ __('tooltip.how_many_hours_remaining') }}">
&nbsp;<i class="fa fa-question-circle"></i>&nbsp;
</a>
</div>
</div>
<div class="form-group">
<label class=" control-label">{!! __('label.booked_hours') !!}</label>
<div class="">
<x-global::forms.text-input disabled="disabled"
value="{{ $timesheetsAllHours }}" style="width:90px;" />
</div>
</div>
<div class="form-group">
<label class=" control-label">{!! __('label.actual_hours_remaining') !!}</label>
<div class="">
<x-global::forms.text-input disabled="disabled" value="{{ $remainingHours }}" style="width:90px;" />
</div>
</div>
</div>
</div>
</div>
@dispatchEvent('beforeEndRightColumn', ['ticket' => $ticket])
</div>
</div>
<script>
jQuery(document).ready(function(){
//Set accordion states
//All accordions start open
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initComplexEditor();
// Hide the editor wrapper initially
jQuery('#descriptionEditor .tiptap-wrapper').hide();
}
});
// Initialize Tiptap complex editor
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initComplexEditor();
}
jQuery(".viewDescription").click(function(e){
if(!jQuery(e.target).is("a")) {
e.stopPropagation();
jQuery(this).hide();
jQuery('#descriptionEditor').show('fast',
function() {
// Show the Tiptap editor wrapper
jQuery('#descriptionEditor .tiptap-wrapper').show();
}
);
}
});
// Initialize recurring task dropdown
jQuery(document).ready(function($) {
$('.recurring-toggle').click(function(e) {
e.preventDefault();
e.stopPropagation();
const $dropdown = $('#recurringTaskForm');
if (!$dropdown.hasClass('loaded')) {
$dropdown.load('{{ BASE_URL }}/hx/recurringTasks/form?entityId={{ $ticket->id }}&module=tickets', function() {
$dropdown.addClass('loaded');
});
}
$dropdown.toggleClass('show');
});
$(document).click(function(e) {
if (!$(e.target).closest('.recurring-dropdown').length) {
$('.recurring-dropdown').removeClass('show');
}
});
});
Prism.highlightAll();
</script>

View File

@@ -0,0 +1,83 @@
<div class="mediamgr_category">
<form action='{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}#files' method='POST' enctype="multipart/form-data" class="formModal">
<div class="par f-left" style="margin-right: 15px;">
<input type="hidden" name="upload" value="1" />
<div class='fileupload fileupload-new' data-provides='fileupload'>
<input type="hidden" />
<div class="input-append">
<div class="uneditable-input span3">
<i class="fa-file fileupload-exists"></i><span class="fileupload-preview"></span>
</div>
<span class="btn btn-file">
<span class="fileupload-new">{!! __('buttons.select_file') !!}</span>
<span class='fileupload-exists'>{!! __('buttons.change') !!}</span>
<input type='file' name='file' />
</span>
<a href='#' class='btn fileupload-exists' data-dismiss='fileupload'>{!! __('buttons.remove') !!}</a>
</div>
</div>
</div>
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.upload')" name="upload" />
</form>
<div class="clear"></div>
</div>
<div class="mediamgr_content">
<ul id='medialist' class='listfile'>
@foreach ($files as $file)
<li class="{{ $file['moduleId'] }}">
<div class="inlineDropDownContainer dropright" style="float:right;">
<a href="javascript:void(0);" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu">
<li class="nav-header">{!! __('subtitles.file') !!}</li>
<li><a href="{{ BASE_URL }}/files/get?module={{ $file['module'] }}&encName={{ $file['encName'] }}&ext={{ $file['extension'] }}&realName={{ $file['realName'] }}" target="_blank">{!! __('links.download') !!}</a></li>
@if ($login::userIsAtLeast($roles::$editor))
<li><a href="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}?delFile={{ $file['id'] }}" class="delete"><i class="fa fa-trash"></i> {!! __('links.delete') !!}</a></li>
@endif
</ul>
</div>
<a class="cboxElement" href="{{ BASE_URL }}/files/get?module={{ $file['module'] }}&encName={{ $file['encName'] }}&ext={{ $file['extension'] }}&realName={{ $file['realName'] }}" target="_blank">
@if (in_array(strtolower($file['extension']), $imgExtensions))
<img style='max-height: 50px; max-width: 70px;' src="{{ BASE_URL }}/files/get?module={{ $file['module'] }}&encName={{ $file['encName'] }}&ext={{ $file['extension'] }}&realName={{ $file['realName'] }}" alt="" />
@else
<div style="font-size:50px; margin-bottom:10px;">
<span class="fa fa-file"></span>
</div>
@endif
<span class="filename">{{ $file['realName'] }}</span>
</a>
</li>
@endforeach
<br class="clearall" />
</ul>
</div><!--mediamgr_content-->
@if (count($files) == 0)
<div class="text-center">
<div style='width:33%' class='svgContainer'>
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_image__folder_re_hgp7.svg') !!}
{!! __('text.no_files') !!}
</div>
</div>
@endif
<div style='clear:both'>&nbsp;</div>
<script type='text/javascript'>
leantime.replaceSVGColors();
</script>

View File

@@ -0,0 +1,24 @@
@php
use Leantime\Core\Controller\Frontcontroller;
$currentUrlPath = BASE_URL . '/' . str_replace('.', '/', Frontcontroller::getCurrentRoute());
@endphp
@dispatchEvent('beforePageHeaderOpen')
<div class="pageheader">
@dispatchEvent('afterPageHeaderOpen')
<div class="pageicon">
<span class="fa fa fa-briefcase"></span>
</div>
<div class="pagetitle">
<h1>{!! __('headlines.my_projects') !!}
</h1>
</div>
@dispatchEvent('beforePageHeaderClose')
</div><!--pageheader-->
@dispatchEvent('afterPageHeaderClose')

View File

@@ -0,0 +1,96 @@
{{-- 关联资源区块:待办事项关联 BOM/工艺文件/工具清单/文件/wiki --}}
<div class="form-group">
<label class="control-label"><i class="fa fa-link"></i> {!! __('label.linked_resources', '关联资源') !!}</label>
{{-- 已关联资源 chips --}}
<div id="linkedResourcesList" style="margin-bottom:6px;">
@forelse (($linkedResources ?? []) as $lr)
<span class="label label-info" style="margin:2px 4px 2px 0; display:inline-block;">
{{ $lr['typeName'] ?? '' }}: {{ $lr['title'] ?? '' }}
<a href="javascript:void(0)" class="btnUnlinkResource" data-type="{{ $lr['type'] }}" data-id="{{ $lr['id'] }}" style="color:#fff;">&times;</a>
</span>
@empty
<em class="text-muted">{{ __('text.no_linked_resources', '暂未关联资源') }}</em>
@endforelse
</div>
{{-- 添加关联 --}}
<div class="row" style="margin:0;">
<select id="resourceTypeSelect" class="span5" style="width:40%;">
<option value="">选择类型</option>
@foreach (($resourceCandidates ?? []) as $group)
<option value="{{ $group['type'] }}">{{ $group['typeName'] }}</option>
@endforeach
</select>
<select id="resourceIdSelect" class="span7" style="width:55%; margin-left:5px;" disabled>
<option value="">先选类型</option>
</select>
</div>
<div style="margin-top:6px;">
<button type="button" class="btn btn-xs btn-default" id="btnAddResource"><i class="fa fa-plus"></i> 添加关联</button>
</div>
</div>
<script>
jQuery(document).ready(function () {
var TICKET_ID = {{ $ticket->id ?? 0 }};
var RESOURCE_CANDIDATES = {!! json_encode($resourceCandidates ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) !!};
var BASE_URL = '{{ BASE_URL }}';
var csrf = function () { return jQuery('meta[name="csrf-token"]').attr('content') || ''; };
// 类型切换 -> 填充资源下拉
jQuery('#resourceTypeSelect').on('change', function () {
var type = jQuery(this).val();
var $id = jQuery('#resourceIdSelect');
$id.empty();
if (!type) { $id.append('<option value="">先选类型</option>').prop('disabled', true); return; }
var group = RESOURCE_CANDIDATES.filter(function (g) { return g.type === type; })[0];
if (!group || !group.items || !group.items.length) {
$id.append('<option value="">(无可用)</option>').prop('disabled', true);
return;
}
group.items.forEach(function (it) {
$id.append('<option value="' + it.id + '">' + it.title + '</option>');
});
$id.prop('disabled', false);
});
// 添加关联
jQuery('#btnAddResource').on('click', function () {
var type = jQuery('#resourceTypeSelect').val();
var resourceId = jQuery('#resourceIdSelect').val();
if (!type || !resourceId) { alert('请选择类型和资源'); return; }
jQuery.ajax({
url: BASE_URL + '/tickets/resource-api/' + TICKET_ID + '/link',
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrf() },
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({ type: type, resourceId: parseInt(resourceId, 10) })
}).done(function () {
location.reload();
}).fail(function (xhr) {
var m = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : ('HTTP ' + xhr.status);
alert(m);
});
});
// 解除关联
jQuery('#linkedResourcesList').on('click', '.btnUnlinkResource', function () {
if (!confirm('确定解除该资源关联?')) { return; }
var type = jQuery(this).data('type');
var id = jQuery(this).data('id');
jQuery.ajax({
url: BASE_URL + '/tickets/resource-api/' + TICKET_ID + '/unlink/' + type + '/' + id,
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': csrf() },
dataType: 'json'
}).done(function () {
location.reload();
}).fail(function (xhr) {
var m = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : ('HTTP ' + xhr.status);
alert(m);
});
});
});
</script>

View File

@@ -0,0 +1,128 @@
<p>{!! __('text.what_are_subtasks') !!}<br /><br /></p>
<ul class="sortableTicketList" style="margin-bottom:120px;">
<li class="">
<a href="javascript:void(0);" class="quickAddLink" id="subticket_new_link" onclick="jQuery('#subticket_new').toggle('fast', function() {jQuery(this).find('input[name=headline]').focus();}); jQuery(this).toggle('fast');"><i class="fas fa-plus-circle"></i> {!! __('links.quick_add_todo') !!}</a>
<div class="ticketBox hideOnLoad" id="subticket_new" >
<form method="post" class="form-group formModal" action="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}#substasks">
<input type="hidden" value="new" name="subtaskId" />
<input type="hidden" value="1" name="subtaskSave" />
<x-global::forms.text-input name="headline" title="{{ __('label.headline') }}" style="width:100%" placeholder="{{ __('input.placeholders.what_are_you_working_on') }}" />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="quickadd" />
<input type="hidden" name="dateToFinish" id="dateToFinish" value="" />
<input type="hidden" name="status" value="3" />
<input type="hidden" name="sprint" value="{{ session('currentSprint') }}" />
<x-global::forms.button tag="a" link="javascript:void(0);" onclick="jQuery('#subticket_new').toggle('fast'); jQuery('#subticket_new_link').toggle('fast');" contentRole="tertiary">
{!! __('links.cancel') !!}
</x-global::forms.button>
</form>
<div class="clearfix"></div>
</div>
</li>
@php
$sumPlanHours = 0;
$sumEstHours = 0;
@endphp
@foreach ($allSubTasks as $subticket)
@php
$sumPlanHours = $sumPlanHours + $subticket['planHours'];
$sumEstHours = $sumEstHours + $subticket['hourRemaining'];
if ($subticket['dateToFinish'] == '0000-00-00 00:00:00' || $subticket['dateToFinish'] == '1969-12-31 00:00:00') {
$date = __('text.anytime');
} else {
$date = new DateTime($subticket['dateToFinish']);
$date = $date->format(__('language.dateformat'));
}
@endphp
<li class="ui-state-default" id="ticket_{{ $subticket['id'] }}" >
<div class="ticketBox fixed priority-border-{{ $subticket['priority'] }}" data-val="{{ $subticket['id'] }}" >
<div class="row">
<div class="col-md-12" style="padding:0 15px;">
@if ($login::userIsAtLeast($roles::$editor))
<div class="inlineDropDownContainer" >
<a href="javascript:void(0)" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu">
<li><a href="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}?delSubtask={{ $subticket['id'] }}" class="delete formModal"><i class="fa fa-trash"></i> {!! __('links.delete_todo') !!}</a></li>
</ul>
</div>
@endif
<a href="#/tickets/showTicket/{{ $subticket['id'] }}">{{ $tpl->escape($subticket['headline']) }}</a>
</div>
</div>
<div class="row">
<div class="col-md-9" style="padding:0 15px;">
<div class="row">
<div class="col-md-4">
{!! __('label.due') !!}<input type="text" title="{{ __('label.due') }}" value="{{ $date }}" class="duedates secretInput quickDueDates" data-id="{{ $subticket['id'] }}" name="date" />
</div>
<div class="col-md-4">
{!! __('label.planned_hours') !!}<input type="text" value="{{ $subticket['planHours'] }}" name="planHours" data-label="planHours-{{ $subticket['id'] }}" class="small-input secretInput asyncInputUpdate" style="width:40px"/>
</div>
<div class="col-md-4">
{!! __('label.estimated_hours_remaining') !!}<input type="text" value="{{ $subticket['hourRemaining'] }}" name="hourRemaining" data-label="hourRemaining-{{ $subticket['id'] }}" class="small-input secretInput asyncInputUpdate" style="width:40px"/>
</div>
</div>
</div>
<div class="col-md-3" style="padding-top: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{{ $subticket['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="text">@if ($subticket['storypoints'] != '' && $subticket['storypoints'] > 0){{ $efforts['' . $subticket['storypoints']] }}@else{!! __('label.story_points_unkown') !!}@endif</span>
&nbsp;<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="effortDropdownMenuLink{{ $subticket['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='{{ $subticket['id'] }}_{{ $effortKey }}' id='ticketEffortChange{{ $subticket['id'] }}{{ $effortKey }}'>{{ $effortValue }}</a>
</li>
@endforeach
</ul>
</div>
@php
if (isset($statusLabels[$subticket['status']])) {
$class = $statusLabels[$subticket['status']]['class'];
$name = $statusLabels[$subticket['status']]['name'];
} else {
$class = 'label-important';
$name = 'new';
}
@endphp
<div class="dropdown ticketDropdown statusDropdown colorized show">
<a class="dropdown-toggle f-left status {{ $class }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $subticket['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="text">{{ $name }}</span>
&nbsp;<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{{ $subticket['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='{{ $tpl->escape($label['name']) }}' data-value='{{ $subticket['id'] }}_{{ $key }}_{{ $label['class'] }}' id='ticketStatusChange{{ $subticket['id'] }}{{ $key }}' >{{ $tpl->escape($label['name']) }}</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
</div>
</li>
@endforeach
</ul>

View File

@@ -0,0 +1,56 @@
@php
use Leantime\Core\Controller\Frontcontroller;
if (!function_exists('findActive')) {
function findActive($route): string
{
if (str_contains(Frontcontroller::getCurrentRoute(), $route)) {
return 'active';
}
return '';
}
}
// Program boards inject their own kanban/table/list URLs + the route fragments used to
// highlight the active tab. Per-project views fall back to the core /tickets/* routes.
$boardTabs = $boardTabs ?? [
'kanban' => ['url' => BASE_URL . '/tickets/showKanban', 'active' => 'Kanban'],
'table' => ['url' => BASE_URL . '/tickets/showAll', 'active' => 'showAll'],
'list' => ['url' => BASE_URL . '/tickets/showList', 'active' => 'showList'],
];
@endphp
<div class="lt-tabs lt-tabs--floating lt-tabs--links hideOnPrint">
<nav class="lt-tabs-group" aria-label="{{ __('links.kanban') }} / {{ __('links.table') }} / {{ __('links.list') }}">
<ul>
<li class="{{ findActive($boardTabs['kanban']['active']) }}">
<a href="{{ $boardTabs['kanban']['url'] }}{{ $searchParams }}" preload="mouseover">
{!! __('links.kanban') !!}
</a>
</li>
<li class="{{ findActive($boardTabs['table']['active']) }}">
<a href="{{ $boardTabs['table']['url'] }}{{ $searchParams }}" preload="mouseover">
{!! __('links.table') !!}
</a>
</li>
<li class="{{ findActive($boardTabs['list']['active']) }}">
<a href="{{ $boardTabs['list']['url'] }}{{ $searchParams }}" preload="mouseover">
{!! __('links.list') !!}
</a>
</li>
</ul>
</nav>
{{-- Board actions (New / Filter / Group By) live on the right of the nav bar
like the report's period picker — so the bar is balanced and the board
needs no separate toolbar row below it. Guarded on $searchCriteria so the
partial stays safe if the nav is ever reused without the board context. --}}
@isset($searchCriteria)
<div class="lt-tabs-actions">
@dispatchEvent('filters.afterLefthandSectionOpen')
@include('tickets::submodules.ticketNewBtn')
@include('tickets::submodules.ticketFilter')
@dispatchEvent('filters.beforeLefthandSectionClose')
</div>
@endisset
</div>

View File

@@ -0,0 +1,354 @@
<input type="hidden" value="{{ $ticket->id }}" name="id" autocomplete="off" readonly/>
<div class="row">
<div class="col-md-9">
<div class="row marginBottom">
<div class="col-md-12">
<div class="form-group">
<x-global::forms.text-input value="{{ $ticket->headline }}" name="headline" variant="headline" autocomplete="off" style="width:99%; margin-bottom:10px;" placeholder="{{ __('input.placeholders.enter_title_of_todo') }}" />
</div>
<!-- Status -->
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.todo_status') !!}</label>
<div class="">
<select
id="status-select"
class=""
name="status"
data-placeholder="{{ isset($ticket->status) ? $statusLabels[$ticket->status]['name'] ?? '' : '' }}"
>
@foreach ($statusLabels as $key => $label)
<option value="{{ $key }}"
@if ($ticket->status == $key) selected='selected' @endif
>{{ $label['name'] }}</option>
@endforeach
</select>
</div>
</div>
<!-- Priority -->
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.priority') !!}</label>
<div class="">
<select id='priority' name='priority' class="">
<option value="">{!! __('label.priority_not_defined') !!}</option>
@foreach ($priorities as $priorityKey => $priorityValue)
<option value="{{ $priorityKey }}"
@if ($priorityKey == $ticket->priority) selected='selected' @endif
>{{ $priorityValue }}</option>
@endforeach
</select>
</div>
</div>
<!-- Effort -->
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.effort') !!}</label>
<div class="">
<select id='storypoints' name='storypoints' class="">
<option value="">{!! __('label.effort_not_defined') !!}</option>
@foreach ($efforts as $effortKey => $effortValue)
<option value="{{ $effortKey }}"
@if ($effortKey == $ticket->storypoints) selected='selected' @endif
>{{ $effortValue }}</option>
@endforeach
</select>
</div>
</div>
<!-- Editor -->
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.editor') !!}</label>
<div class="">
<select data-placeholder="{{ __('label.filter_by_user') }}" style="width:175px;"
name="editorId" id="editorId" class="user-select tw-mr-sm">
<option value="">{!! __('label.not_assigned_to_user') !!}</option>
@foreach ($users as $userRow)
<option value="{{ $userRow['id'] }}"
@if ($ticket->editorId == $userRow['id']) selected='selected' @endif
>{{ $userRow['firstname'] . ' ' . $userRow['lastname'] }}</option>
@endforeach
</select>&nbsp;
</div>
<div style="padding-top:6px;">
@if ($login::userIsAtLeast($roles::$editor))
<a href="javascript:void(0);" onclick="jQuery('#editorId').val({{ session('userdata.id') }}).trigger('chosen:updated');">{!! __('label.assign_to_me') !!}</a>
@endif
</div>
</div>
<!-- Collaborators -->
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.collaborators') !!}</label>
<div class="">
<select data-placeholder="{{ __('label.filter_by_user') }}"
style="width:175px;"
name="collaborators[]"
id="collaborators"
class="user-select tw-mr-sm"
multiple>
@foreach ($users as $userRow)
<option value="{{ $userRow['id'] }}"
@if (in_array($userRow['id'], $ticket->collaborators ?? [])) selected='selected' @endif
>
{{ $userRow['firstname'] . ' ' . $userRow['lastname'] }}
</option>
@endforeach
</select>
</div>
</div>
<!-- Due Date -->
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.due_date') !!}</label>
<div class="">
<input type="text" class="dates" style="width:110px;" id="deadline" autocomplete="off"
value="{{ format($ticket->dateToFinish)->date() }}"
name="dateToFinish" placeholder="{{ __('language.dateformat') }}"/>
<input type="time" class="timepicker tw-mr-sm" style="width:120px;" id="dueTime" autocomplete="off"
value="{{ format($ticket->dateToFinish)->time24() }}"
name="timeToFinish"/>
</div>
<div style="padding-top:6px;">
@dispatchEvent('afterDates', ['ticket' => $ticket])
</div>
</div>
<div class="form-group tw-flex tw-w-3/5">
<label class="control-label tw-mx-m tw-w-[100px]">{!! __('label.tags') !!}</label>
<div class="">
<input type="text" value="{{ $ticket->tags }}" name="tags" id="tags" />
</div>
</div>
<br />
<div class="form-group" id="descriptionEditor">
<textarea name="description" id="ticketDescription"
class="tiptapComplex">{!! $ticket->description !== null ? htmlentities($ticket->description) : '' !!}</textarea><br/>
</div>
<input type="hidden" name="acceptanceCriteria" value=""/>
</div>
</div>
<div class="sticky-modal-footer">
<div class="row">
<div class="col-md-12" style="margin-top:15px;">
<input type="hidden" name="saveTicket" value="1" />
<input type="hidden" id="saveAndCloseButton" name="saveAndCloseTicket" value="0" />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="saveTicket" class="saveTicketBtn" />
<x-global::forms.button tag="input" inputType="submit" variant="outline" name="saveAndCloseTicket" onclick="jQuery('#saveAndCloseButton').val('1');" :labelText="__('buttons.save_and_close')" />
</div>
</div>
</div>
@if ($ticket->id)
<br />
<hr />
@dispatchEvent('beforeSubtasks', ['ticketId' => $ticket->id])
<h4 class="widgettitle title-light"><i class="fa-solid fa-sitemap"></i> {!! __('subtitles.subtasks') !!}</h4>
<x-global::hx
wrapperId="ticketSubtasks"
:for="\Leantime\Domain\Tickets\Hxcontrollers\Subtasks::class"
:id="$ticket->id"
trigger="load"
indicator=".subtaskIndicator"
/>
<div class="htmx-indicator subtaskIndicator">
Loading Subtasks ...<br /><br />
</div>
<h4 class="widgettitle title-light"><span
class="fa-solid fa-comments"></span>{!! __('subtitles.discussion') !!}</h4>
<div class="row-fluid">
<form method="post" action="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}" class="formModal">
<input type="hidden" name="comment" value="1" />
@include('comments::submodules.generalComment', ['formUrl' => BASE_URL . '/tickets/showTicket/' . $ticket->id])
</form>
</div>
@endif
</div>
<div class="col-md-3">
<div class="row marginBottom">
<div class="col-md-12">
<h5 class="accordionTitle" id="accordion_link_tickets-organization" style="padding-bottom:15px; font-size:var(--font-size-l)">
<a href="javascript:void(0)"
class="accordion-toggle"
id="accordion_toggle_tickets-organization"
onclick="leantime.snippets.accordionToggle('tickets-organization');">
<i class="fa fa-angle-down"></i>
<span class="fa fa-folder-open"></span>
{!! __('subtitles.organization') !!}
</a>
</h5>
<div class="simpleAccordionContainer" id="accordion_content-tickets-organization" style="padding-left:0">
<!-- Type -->
<div class="form-group">
<label class="control-label">{!! __('label.todo_type') !!}</label>
<div class="">
<select id='type' name='type' class="span11">
@foreach ($ticketTypes as $types)
<option value="{{ strtolower($types) }}"
@if (strtolower($types) == strtolower($ticket->type ?? '')) selected='selected' @endif
>{!! __('label.' . strtolower($types)) !!}</option>
@endforeach
</select><br/>
</div>
</div>
<!-- Project -->
<div class="form-group">
<label class="control-label">{!! __('label.project') !!}</label>
<select name="projectId" class="tw-w-full">
@foreach ($allAssignedprojects as $project)
<option value="{{ $project['id'] }}"
@if ($ticket->projectId == $project['id'])
selected
@elseif (session('currentProject') == $project['id'])
selected
@endif
>{{ $tpl->escape($project['name']) }}</option>
@endforeach
</select>
</div>
<!-- Milestones -->
<div class="form-group">
<label class="control-label">{!! __('label.milestone') !!}</label>
<div class="">
<div class="form-group">
<select name="milestoneid" class="span11" >
<option value="">{!! __('label.not_assigned_to_milestone') !!}</option>
@foreach ($milestones as $milestoneRow)
<option value="{{ $milestoneRow->id }}"
@if ($ticket->milestoneid == $milestoneRow->id) selected='selected' @endif
>{{ $tpl->escape($milestoneRow->headline) }}</option>
@endforeach
</select>
</div>
</div>
</div>
<!-- Sprint -->
<div class="form-group">
<label class="control-label">{!! __('label.sprint') !!}</label>
<div class="">
<select id="sprint-select" class="span11" name="sprint"
data-placeholder="{{ $ticket->sprint }}">
<option value="">{!! __('label.backlog') !!}</option>
@if ($sprints)
@foreach ($sprints as $sprintRow)
<option value="{{ $sprintRow->id }}"
@if ($ticket->sprint == $sprintRow->id) selected='selected' @endif
>{{ $sprintRow->name }}@if (! empty($sprintRow->isInherited)) {{ __('label.program_sprint') }} @endif</option>
@endforeach
@endif
</select>
</div>
</div>
<!-- Related -->
<div class="form-group">
<label class="control-label">{!! __('label.related_to') !!}</label>
<div class="">
<div class="form-group">
<select name="dependingTicketId" class="span11" >
<option value="">{!! __('label.not_related') !!}</option>
@if (is_array($ticketParents))
@foreach ($ticketParents as $ticketRow)
<option value="{{ $ticketRow->id }}"
@if ($ticket->dependingTicketId == $ticketRow->id) selected='selected' @endif
>{{ $tpl->escape($ticketRow->headline) }}</option>
@endforeach
@endif
</select>
</div>
</div>
</div>
<!-- Linked Resources -->
@include('tickets::submodules.resourceLinks')
</div>
</div>
</div>
<div class="row marginBottom">
<div class="col-md-12">
<h5 class="accordionTitle" id="accordion_link_tickets-dates" style="padding-bottom:15px; font-size:var(--font-size-l)">
<a href="javascript:void(0)"
class="accordion-toggle"
id="accordion_toggle_tickets-dates"
onclick="leantime.snippets.accordionToggle('tickets-dates');">
<i class="fa fa-angle-down"></i>
<span class="fa fa-calendar"></span>
{!! __('subtitles.schedule') !!}
</a>
</h5>
<div class="simpleAccordionContainer" id="accordion_content-tickets-dates" style="padding-left:0">
<div class="form-group">
<label class=" control-label">{!! __('label.working_date_from') !!}</label>
<div class="">
<input type="text" class="editFrom" style="width:100px;" name="editFrom" autocomplete="off"
value="{{ format($ticket->editFrom)->date() }}" placeholder="{{ __('language.dateformat') }}"/>
<input type="time" class="timepicker" style="width:120px;" id="timeFrom" autocomplete="off"
value="{{ format($ticket->editFrom)->time24() }}"
name="timeFrom"/>
</div>
</div>
<div class="form-group">
<label class=" control-label">{!! __('label.working_date_to') !!}</label>
<div class="">
<input type="text" class="editTo" style="width:100px;" name="editTo" autocomplete="off"
value="{{ format($ticket->editTo)->date() }}" placeholder="{{ __('language.dateformat') }}"/>
<input type="time" class="timepicker" style="width:120px;" id="timeTo" autocomplete="off"
value="{{ format($ticket->editTo)->time24() }}"
name="timeTo"/>
</div>
</div>
<div class="form-group">
<label class=" control-label">{!! __('label.planned_hours') !!} / {!! __('label.estimated_hours_remaining') !!}</label>
<div class="">
<x-global::forms.text-input value="{{ $ticket->planHours }}" name="planHours" style="width:45px;" />&nbsp;/&nbsp;
<x-global::forms.text-input value="{{ $ticket->hourRemaining }}" name="hourRemaining" style="width:45px;" />
<a href="javascript:void(0)" class="infoToolTip" data-placement="left" data-toggle="tooltip" data-tippy-content="{{ __('tooltip.how_many_hours_remaining') }}">
&nbsp;<i class="fa fa-question-circle"></i>&nbsp;
</a>
</div>
</div>
</div>
</div>
</div>
@dispatchEvent('beforeEndRightColumn', ['ticket' => $ticket])
</div>
</div>
<script>
jQuery(document).ready(function(){
//Set accordion states
//All accordions start open
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initComplexEditor();
}
Prism.highlightAll();
});
</script>

View File

@@ -0,0 +1,249 @@
@php
use Leantime\Core\Controller\Frontcontroller;
$currentRoute = Frontcontroller::getCurrentRoute();
$currentUrlPath = BASE_URL . '/' . str_replace('.', '/', $currentRoute);
// Program boards render this partial under a /pgmPro/* route and can pass an explicit
// self-redirect target. Default to the current route for normal per-project views.
$searchFormUrl = $searchFormUrl ?? $currentUrlPath;
$groupBy = $groupByOptions;
$sortBy = $sortOptions;
$statusLabels = $allTicketStates;
$taskToggle = $enableTaskTypeToggle ?? false;
// Multi-project (program) context: show the project filter + the "project" group-by.
$showProjectFilter = isset($availableProjects);
// Kanban shows status as columns, so the status group-by is suppressed. Program kanban
// templates set $isKanbanView; per-project views derive it from the route.
$isKanbanView = $isKanbanView ?? ($currentRoute === 'tickets.showKanban');
@endphp
<form action="" method="get" id="ticketSearch">
<input type="hidden" value="1" name="search"/>
@unless ($showProjectFilter)
<input type="hidden" value="{{ session('currentProject') }}" name="projectId" id="projectIdInput"/>
@endunless
<div class="filterWrapper" style="display:inline-block; position:relative; vertical-align: bottom; margin-bottom:20px;">
{{-- Kept as a raw <a> (not forms.button): this button is whitespace-sensitive the
</a>@if adjacency below avoids an inline-block gap, and the component would reintroduce
surrounding whitespace. Migrate once the component guarantees whitespace-tight output. --}}
<a class="btn btn-link" onclick="leantime.ticketsController.toggleFilterBar();" style="margin-right:5px;"
data-tippy-content="{{ __('popover.filter') }}">
<i class="fas fa-filter"></i> Filter{!! $numOfFilters > 0 ? " <span class='badge badge-primary'>" . $numOfFilters . '</span> ' : '' !!}
{{-- Please don't change the code formatting below, if not right next to each other it somehow adds a space between the two buttons and increases the distance --}}
</a>@if ($currentRoute !== 'tickets.roadmap' && $currentRoute != 'tickets.showProjectCalendar')<div class="btn-group viewDropDown">
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" data-tippy-content="{{ __('popover.group_by') }}">
<span class="fa-solid fa-diagram-project"></span> Group By
@if ($searchCriteria['groupBy'] != 'all' && $searchCriteria['groupBy'] != '')
<span class="badge badge-primary">1</span>
@endif
</button>
<ul class="dropdown-menu">
@foreach ($groupBy as $input)
@if ($input['field'] === 'status' && $isKanbanView)
@continue
@endif
{{-- "Group by project" only makes sense on a multi-project (program) board. --}}
@if ($input['field'] === 'projectId' && ! $showProjectFilter)
@continue
@endif
<li>
<span class="radio">
<input
type="radio"
name="groupBy"
@if ($searchCriteria['groupBy'] == $input['field']) checked='checked' @endif
value="{{ $input['field'] }}"
id="{{ $input['id'] }}"
onclick="leantime.ticketsController.initTicketSearchUrlBuilder('{{ $searchFormUrl }}')"
/>
<label for="{{ $input['id'] }}">{!! __("label.{$input['label']}") !!}</label>
</span>
</li>
@endforeach
</ul>
</div>
@endif
<div class="filterBar hideOnLoad" style="width:250px;">
<div class="row-fluid">
@dispatchEvent('filters.beforeFirstBarField')
@if ($showProjectFilter)
<div class="">
<label class="inline">{!! __('label.project') !!}</label>
<div class="form-group">
<select data-placeholder="{{ __('label.project') }}" title="{{ __('label.project') }}" name="projects" multiple="multiple" class="project-select" id="projectsSelect">
<option value="" data-placeholder="true">{!! __('label.project') !!}</option>
@foreach ($availableProjects as $projectFilterId => $projectFilterName)
<option value="{{ $projectFilterId }}"
@if (isset($searchCriteria['projects']) && in_array((string) $projectFilterId, explode(',', (string) $searchCriteria['projects']), true)) selected='selected' @endif
>{{ $tpl->escape($projectFilterName) }}</option>
@endforeach
</select>
</div>
</div>
@endif
<div class="">
<label class="inline">{!! __('label.user') !!}</label>
<div class="form-group">
<select data-placeholder="{{ __('input.placeholders.filter_by_user') }}" title="{{ __('input.placeholders.filter_by_user') }}" name="users" multiple="multiple" class="user-select" id="userSelect">
<option value="" data-placeholder="true">All Users</option>
@foreach ($users as $userRow)
<option value="{{ $userRow['id'] }}"
@if ($searchCriteria['users'] !== false && $searchCriteria['users'] !== null && array_search($userRow['id'], explode(',', $searchCriteria['users'])) !== false) selected='selected' @endif
>{!! sprintf(__('text.full_name'), $tpl->escape($userRow['firstname']), $tpl->escape($userRow['lastname'])) !!}</option>
@endforeach
</select>
</div>
</div>
<div class="">
<label class="inline">{!! __('label.milestone') !!}</label>
<div class="form-group">
<select data-placeholder="{{ __('input.placeholders.filter_by_milestone') }}" multiple="multiple" title="{{ __('input.placeholders.filter_by_milestone') }}" name="milestone" id="milestoneSelect">
<option value="" data-placeholder="true">{!! __('label.all_milestones') !!}</option>
<option value="0" @if (isset($searchCriteria['milestone']) && in_array('0', explode(',', (string) $searchCriteria['milestone']), true)) selected='selected' @endif>{!! __('label.not_assigned_to_milestone') !!}</option>
@if (is_array($milestones))
@foreach ($milestones as $milestoneRow)
<option value="{{ $milestoneRow->id }}"
@if (isset($searchCriteria['milestone']) && ($searchCriteria['milestone'] == $milestoneRow->id) && array_search($milestoneRow->id, explode(',', $searchCriteria['milestone'])) !== false) selected='selected' @endif
>{{ $tpl->escape($milestoneRow->headline) }}</option>
@endforeach
@endif
</select>
</div>
</div>
<div class="">
<label class="inline">{!! __('label.todo_type') !!}</label>
<div class="form-group">
<select multiple="multiple" data-placeholder="{{ __('input.placeholders.filter_by_type') }}" title="{{ __('input.placeholders.filter_by_type') }}" name="type" id="typeSelect">
<option value="" data-placeholder="true">{!! __('label.all_types') !!}</option>
@foreach ($types as $type)
<option value="{{ $type }}"
@if (isset($searchCriteria['type']) && array_search($type, explode(',', $searchCriteria['type'])) !== false) selected='selected' @endif
>{{ $type }}</option>
@endforeach
</select>
</div>
</div>
<div class="">
<label class="inline">{!! __('label.todo_priority') !!}</label>
<div class="form-group">
<select multiple="multiple" data-placeholder="{{ __('input.placeholders.filter_by_priority') }}" title="{{ __('input.placeholders.filter_by_priority') }}" name="priority" id="prioritySelect">
<option value="" data-placeholder="true">{!! __('label.all_priorities') !!}</option>
@foreach ($priorities as $priorityKey => $priorityValue)
<option value="{{ $priorityKey }}"
@if (isset($searchCriteria['priority']) && array_search($priorityKey, explode(',', $searchCriteria['priority'])) !== false) selected='selected' @endif
>{{ $priorityValue }}</option>
@endforeach
</select>
</div>
</div>
<div class="">
<label class="inline">{!! __('label.todo_status') !!}</label>
<div class="form-group">
<select multiple="multiple" data-placeholder="{{ __('input.placeholders.filter_by_status') }}" name="status" multiple="multiple" class="status-select" id="statusSelect">
<option value="" data-placeholder="true">All Statuses</option>
<option value="not_done" @if ($searchCriteria['status'] !== false && str_contains($searchCriteria['status'], 'not_done')) selected='selected' @endif>{!! __('label.not_done') !!}</option>
@foreach ($statusLabels as $key => $label)
<option value="{{ $key }}"
@if ($searchCriteria['status'] !== false && array_search((string) $key, explode(',', $searchCriteria['status'])) !== false) selected='selected' @endif
>{{ $tpl->escape($label['name']) }}</option>
@endforeach
</select>
</div>
</div>
<div class="">
<div class="form-group">
<label class="inline">{!! __('label.search_term') !!}</label>
<x-global::forms.text-input name="termInput" id="termInput"
style="width: 230px"
value="{{ $searchCriteria['term'] }}"
placeholder="{{ __('label.search_term') }}" />
</div>
</div>
<div class="" style="margin-top:15px;">
<x-global::forms.button tag="input" inputType="submit" :labelText="__('buttons.search')" name="search" class="form-control" contentRole="primary" />
</div>
</div>
</div>
@if (isset($taskToggle) && $taskToggle === true)
<div class="" style="float:right; margin-left:5px; ">
<input type="checkbox" class="toggle" id="taskTypeToggle" onchange="jQuery('#ticketSearch').submit();" name="showTasks" value="true" {{ ($showTasks === 'true') ? 'checked="checked"' : '' }} style="margin-right:5px;" />
<label style="text-wrap: nowrap; float:right;">Show Tasks</label>
</div>
@endif
</div>
<div class="clearall"></div>
@dispatchEvent('filters.beforeBar')
@dispatchEvent('filters.beforeFormClose')
</form>
<script>
jQuery(document).ready(function() {
new SlimSelect({
select: '#userSelect',
settings: {
placeholderText: 'All Users',
},
});
new SlimSelect({
select: '#milestoneSelect',
settings: {
placeholderText: 'All Milestones',
},
});
new SlimSelect({
select: '#prioritySelect',
settings: {
placeholderText: 'All Priorities',
},
});
new SlimSelect({
select: '#typeSelect',
settings: {
placeholderText: 'All Types',
},
});
new SlimSelect({
select: '#statusSelect',
settings: {
placeholderText: 'All Statuses',
},
});
@if ($showProjectFilter)
new SlimSelect({
select: '#projectsSelect',
settings: {
placeholderText: @js(__('label.project')),
},
});
@endif
leantime.ticketsController.initTicketSearchSubmit('{{ $searchFormUrl }}');
})
</script>

View File

@@ -0,0 +1,124 @@
@php
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Sprints\Models\Sprints;
$currentUrlPath = BASE_URL . '/' . str_replace('.', '/', Frontcontroller::getCurrentRoute());
$currentSprintId = $currentSprint;
$searchSprint = $searchCriteria['sprint'] ?? '';
$sprint = false;
$currentSprintId = $currentSprintId == '' ? 'all' : $currentSprintId;
if ($currentSprintId == 'all') {
$sprint = new Sprints;
$sprint->id = 'all';
$sprint->name = __('links.all_todos');
}
if ($currentSprintId == 'backlog') {
$sprint = new Sprints;
$sprint->id = 'backlog';
$sprint->name = __('links.backlog');
}
if (is_array($sprints)) {
foreach ($sprints as $sprintRow) {
if ($sprintRow->id == $currentSprintId) {
$sprint = $sprintRow;
break;
}
}
}
@endphp
@dispatchEvent('beforePageHeaderOpen')
<div class="pageheader">
@dispatchEvent('afterPageHeaderOpen')
<div class="pageicon">
<span class="fa fa-fw fa-thumb-tack"></span>
</div>
<div class="pagetitle">
{{-- Build "Client // Project" only from the parts that exist, so a
missing client/project never leaves a stray " // ". (`.` binds
tighter than `??`, so the old inline expression mis-grouped.) --}}
@php
$headerParts = array_filter(
[session('currentProjectClient'), session('currentProjectName')],
fn ($part) => $part !== null && $part !== ''
);
@endphp
<h5>{{ implode(' / ', $headerParts) }}</h5>
{{-- Migrated to the shared subject switcher (was a hand-rolled
header-title-dropdown). The sprint menu items stay here they're
domain-specific — but the "To-Dos // <current> ▾" chrome is now the
component. Zero visual change. --}}
<x-global::subjectSwitcher
:parent="__('headlines.todos')"
:current="$sprint !== false ? $sprint->name : __('dropdown.choose_sprint')">
<li><a class="wikiModal inlineEdit" href="#/sprints/editSprint/"><i class="fa-solid fa-plus"></i> {!! __('links.create_sprint_no_icon') !!}</a></li>
<li class='nav-header border'></li>
<li>
<a href="javascript:void(0);" onclick="jQuery('#sprintSelect').val('all'); leantime.ticketsController.initTicketSearchUrlBuilder('{{ $currentUrlPath }}')">{!! __('links.all_todos') !!}</a>
</li>
<li>
<a href="javascript:void(0);" onclick="jQuery('#sprintSelect').val('backlog'); leantime.ticketsController.initTicketSearchUrlBuilder('{{ $currentUrlPath }}')">{!! __('links.backlog') !!}</a>
</li>
@foreach ($sprints as $sprintRow)
<li>
<a href="javascript:void(0);" onclick="jQuery('#sprintSelect').val({{ $sprintRow->id }}); leantime.ticketsController.initTicketSearchUrlBuilder('{{ $currentUrlPath }}')">{{ $tpl->escape($sprintRow->name) }}@if (! empty($sprintRow->isInherited)) <span class="label label-info">{{ __('label.program_sprint') }}</span>@endif<br /><small>{!! sprintf(__('label.date_from_date_to'), format($sprintRow->startDate)->date(), format($sprintRow->endDate)->date()) !!}</small></a>
</li>
@endforeach
</x-global::subjectSwitcher>
<input type="hidden" name="sprintSelect" id="sprintSelect" value="{{ $currentSprintId }}" />
</div>
{{-- Right cluster on the breadcrumb bar: board stats + (for a real sprint
view only) the sprint edit/delete menu. --}}
<div class="pageheader-right">
{{-- Header rule (2026-08-03): LEFT = orientation (where you are),
RIGHT = information (what about it) meta, then status, then ,
one vertically-centered row. --}}
@isset($boardSummary)
@php
// Board metrics as a one-line meta string; segments join with a
// single " · " so it reads cleanly no matter which are present.
$summaryParts = [sprintf(__('label.board_task_count'), $boardSummary->total)];
if ($boardSummary->unassigned > 0) {
$summaryParts[] = sprintf(__('label.board_unassigned'), $boardSummary->unassigned);
}
if ($boardSummary->dueThisWeek > 0) {
$summaryParts[] = sprintf(__('label.board_due_this_week'), $boardSummary->dueThisWeek);
}
if ($boardSummary->lastUpdated !== null) {
$summaryParts[] = sprintf(__('label.board_updated'), $boardSummary->lastUpdated->setToUserTimezone()->diffForHumans());
}
@endphp
<div class="pageheader-meta">{{ implode(' · ', $summaryParts) }}</div>
@endisset
@if (
($currentSprint !== false)
&& ($currentSprint !== null)
&& count($sprints) > 0
&& $currentSprintId != 'all'
&& $currentSprintId != 'backlog'
)
<span class="dropdown dropdownWrapper headerEditDropdown">
<a href="javascript:void(0)" class="dropdown-toggle btn btn-transparent" data-toggle="dropdown"><i class="fa-solid fa-ellipsis-v"></i></a>
<ul class="dropdown-menu editCanvasDropdown">
{{-- Inherited (program-owned) sprints are managed at the program level, never edited
or deleted from a child project. The service IDOR-fences this server-side too.
$sprint can be false (stale/deleted currentSprint), so guard the object access. --}}
@if ($login::userIsAtLeast($roles::$editor) && (! is_object($sprint) || empty($sprint->isInherited)))
<li><a href="#/sprints/editSprint/{{ $currentSprint }}">{!! __('link.edit_sprint') !!}</a></li>
<li><a href="#/sprints/delSprint/{{ $currentSprint }}" class="delete">{!! __('links.delete_sprint') !!}</a></li>
@endif
</ul>
</span>
@endif
</div>
@dispatchEvent('beforePageHeaderClose')
</div><!--pageheader-->
@dispatchEvent('afterPageHeaderClose')

View File

@@ -0,0 +1,15 @@
@if (can('tickets.create') && !empty($newField))
<div class="btn-group pull-left" style="margin-right:5px;">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{!! __('links.new_with_icon') !!} <span class="caret"></span></button>
<ul class="dropdown-menu">
@foreach ($newField as $option)
<li>
<a
href="{{ !empty($option['url']) ? $option['url'] : '' }}"
class="{{ !empty($option['class']) ? $option['class'] : '' }}"
> {!! !empty($option['text']) ? __($option['text']) : '' !!}</a>
</li>
@endforeach
</ul>
</div>
@endif

View File

@@ -0,0 +1,15 @@
@if (can('tickets.create') && !empty($newField))
<div class="btn-group">
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{!! __('links.new_with_icon') !!} <span class="caret"></span></button>
<ul class="dropdown-menu">
@foreach ($newField as $option)
<li>
<a
href="{{ !empty($option['url']) ? $option['url'] : '' }}"
class="{{ !empty($option['class']) ? $option['class'] : '' }}"
> {!! !empty($option['text']) ? __($option['text']) : '' !!}</a>
</li>
@endforeach
</ul>
</div>
@endif

View File

@@ -0,0 +1,88 @@
@php
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Sprints\Models\Sprints;
$currentUrlPath = BASE_URL . '/' . str_replace('.', '/', Frontcontroller::getCurrentRoute());
$currentSprintId = $currentSprint;
$searchSprint = $searchCriteria['sprint'] ?? '';
$sprint = false;
$currentSprintId = $currentSprintId == '' ? 'all' : $currentSprintId;
if ($currentSprintId == 'all') {
$sprint = new Sprints;
$sprint->id = 'all';
$sprint->name = __('links.all_todos');
}
if ($currentSprintId == 'backlog') {
$sprint = new Sprints;
$sprint->id = 'backlog';
$sprint->name = __('links.backlog');
}
if (is_array($sprints)) {
foreach ($sprints as $sprintRow) {
if ($sprintRow->id == $currentSprintId) {
$sprint = $sprintRow;
break;
}
}
}
@endphp
@dispatchEvent('beforePageHeaderOpen')
<div class="pageheader">
@dispatchEvent('afterPageHeaderOpen')
<div class="pageicon">
<span class="fa fa-fw fa-chart-gantt"></span>
</div>
<div class="pagetitle">
@if (($sprints !== false) && ($sprints !== null) && count($sprints) > 0)
<x-global::subjectSwitcher
:parent="__('headline.milestones')"
:current="$sprint !== false ? $sprint->name : __('label.select_board')">
<li><a class="wikiModal inlineEdit" href="#/sprints/editSprint/"><i class="fa-solid fa-plus"></i> {!! __('links.create_sprint_no_icon') !!}</a></li>
<li class='nav-header border'></li>
<li>
<a href="javascript:void(0);" onclick="jQuery('#sprintSelect').val('all'); leantime.ticketsController.initTicketSearchUrlBuilder('{{ $currentUrlPath }}')">{!! __('links.all_todos') !!}</a>
</li>
<li>
<a href="javascript:void(0);" onclick="jQuery('#sprintSelect').val('backlog'); leantime.ticketsController.initTicketSearchUrlBuilder('{{ $currentUrlPath }}')">{!! __('label.backlog') !!}</a>
</li>
@foreach ($sprints as $sprintRow)
<li>
<a href="javascript:void(0);" onclick="jQuery('#sprintSelect').val({{ $sprintRow->id }}); leantime.ticketsController.initTicketSearchUrlBuilder('{{ $currentUrlPath }}')">{{ $tpl->escape($sprintRow->name) }}<br /><small>{!! sprintf(__('label.date_from_date_to'), format($sprintRow->startDate)->date(), format($sprintRow->endDate)->date()) !!}</small></a>
</li>
@endforeach
</x-global::subjectSwitcher>
@else
<h1>{!! __('headline.milestones') !!}</h1>
@endif
<input type="hidden" name="sprintSelect" id="sprintSelect" value="{{ $currentSprintId }}" />
</div>
@if (
($currentSprint !== false)
&& ($currentSprint !== null)
&& count($sprints) > 0
&& $currentSprintId != 'all'
&& $currentSprintId != 'backlog'
)
<div class="pageheader-right">
<span class="dropdown dropdownWrapper headerEditDropdown">
<a href="javascript:void(0)" class="dropdown-toggle btn btn-transparent" data-toggle="dropdown"><i class="fa-solid fa-ellipsis-v"></i></a>
<ul class="dropdown-menu editCanvasDropdown">
{{-- Inherited (program-owned) sprints are managed at the program level only.
$sprint can be false (stale/deleted currentSprint), so guard the object access. --}}
@if ($login::userIsAtLeast($roles::$editor) && (! is_object($sprint) || empty($sprint->isInherited)))
<li><a href="#/sprints/editSprint/{{ $currentSprint }}">{!! __('link.edit_sprint') !!}</a></li>
<li><a href="#/sprints/delSprint/{{ $currentSprint }}" class="delete">{!! __('links.delete_sprint') !!}</a></li>
@endif
</ul>
</span>
</div>
@endif
@dispatchEvent('beforePageHeaderClose')
</div><!--pageheader-->
@dispatchEvent('afterPageHeaderClose')

View File

@@ -0,0 +1,35 @@
@php
use Leantime\Core\Controller\Frontcontroller;
if (!function_exists('findActive')) {
function findActive($route): string
{
if (str_contains(Frontcontroller::getCurrentRoute(), $route)) {
return 'active';
}
return '';
}
}
@endphp
<div class="lt-tabs lt-tabs--floating lt-tabs--links hideOnPrint">
<nav class="lt-tabs-group" aria-label="{{ __('links.timeline') }}">
<ul>
<li class="{{ findActive('roadmap') }}">
<a href="{{ BASE_URL }}/tickets/roadmap{{ $searchParams }}" preload="mouseover">
{!! __('links.timeline') !!}
</a>
</li>
<li class="{{ findActive('showAllMilestones') }}">
<a href="{{ BASE_URL }}/tickets/showAllMilestones{{ $searchParams }}" preload="mouseover">
{!! __('links.table') !!}
</a>
</li>
<li class="{{ findActive('Calendar') }}">
<a href="{{ BASE_URL }}/tickets/showProjectCalendar{{ $searchParams }}" preload="mouseover">
{!! __('links.calendar') !!}
</a>
</li>
</ul>
</nav>
</div>

View File

@@ -0,0 +1,90 @@
@php
$values = $timesheetValues;
if ($remainingHours < 0) {
$remainingHours = 0;
}
$currentPay = $userHours * $userInfo['wage'];
@endphp
<div class="row">
<div class="col-md-6">
<h4 class="widgettitle title-light"><span class="fa fa-clock-o"></span>{!! __('headline.add_time_entry', false) !!}</h4>
<br />
<form method="post" action="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}#timesheet" class="formModal">
<label for="kind">{!! __('label.timesheet_kind') !!}</label>
<span class="field">
<select id="kind" name="kind">
@foreach ($kind as $key => $row)
<option value="{{ $key }}"
@if ($row == $values['kind']) selected="selected" @endif
>{!! __(strtolower($row)) !!}</option>
@endforeach
</select>
</span>
<label for="timesheetdate">{!! __('label.date') !!}:</label>
<input type="text" id="timesheetdate" name="date" class="dates" value="{{ format($values['date'])->date() }}" /><br/>
<label for="hours">{!! __('label.hours') !!}</label>
<span class="field">
<x-global::forms.text-input id="hours" name="hours" value="{{ $values['hours'] }}" size="7" variant="small" />
</span>
<label for="description">{!! __('label.description') !!}</label>
<span class="field">
<x-global::forms.textarea rows="5" cols="50" id="description" name="description">{{ $values['description'] }}</x-global::forms.textarea><br />
</span>
<input type="hidden" name="saveTimes" value="1" />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="saveTimes" />
</form>
</div>
<div class="col-md-6">
<h4 class="widgettitle title-light"><span class="fa fa-bar-chart"></span>{!! __('subtitles.logged_hours_chart') !!}</h4>
<br />
<canvas id="canvas"></canvas>
<p><br />
{!! __('label.planned_hours') !!}: {{ $ticket->planHours }}<br />
{!! __('label.booked_hours') !!}: {{ $timesheetsAllHours }}<br />
{!! __('label.actual_hours_remaining') !!}: {{ $remainingHours }}<br />
</p>
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
var d2 = [];
var d3 = [];
var labels = [];
@php
// Emit every value through json_encode so the generated JS is always
// valid. Raw concatenation broke when a value was null or non-numeric
// (e.g. Postgres SUM()/plan hours formatting), producing a JS syntax
// error that killed the whole time-tracking modal. (#3353)
$sum = 0;
$planHours = is_numeric($ticket->planHours) ? (float) $ticket->planHours : 0;
foreach ($ticketHours as $hours) {
$sum = $sum + (float) ($hours['summe'] ?? 0);
try {
$label = dtHelper()->parseDbDateTime($hours['utc'])->setToUserTimezone()->format('Y-m-d');
echo 'labels.push(' . json_encode($label) . ");\n";
echo 'd2.push(' . json_encode($sum) . ");\n";
echo 'd3.push(' . json_encode($planHours) . ");\n";
} catch (\Exception $e) {
// not much we can do at this point. Ignore the datapoint
}
}
@endphp
leantime.ticketsController.initTimeSheetChart(labels, d2, d3, "canvas")
});
</script>