OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
@@ -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') }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</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>
|
||||
@@ -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'> </div>
|
||||
|
||||
<script type='text/javascript'>
|
||||
leantime.replaceSVGColors();
|
||||
</script>
|
||||
@@ -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')
|
||||
@@ -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;">×</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>
|
||||
128
app/Domain/Tickets/Templates/submodules/subTasks.blade.php
Normal file
128
app/Domain/Tickets/Templates/submodules/subTasks.blade.php
Normal 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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -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>
|
||||
354
app/Domain/Tickets/Templates/submodules/ticketDetails.blade.php
Normal file
354
app/Domain/Tickets/Templates/submodules/ticketDetails.blade.php
Normal 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>
|
||||
</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;" /> /
|
||||
<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') }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</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>
|
||||
249
app/Domain/Tickets/Templates/submodules/ticketFilter.blade.php
Normal file
249
app/Domain/Tickets/Templates/submodules/ticketFilter.blade.php
Normal 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>
|
||||
124
app/Domain/Tickets/Templates/submodules/ticketHeader.blade.php
Normal file
124
app/Domain/Tickets/Templates/submodules/ticketHeader.blade.php
Normal 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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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>
|
||||
90
app/Domain/Tickets/Templates/submodules/timesheet.blade.php
Normal file
90
app/Domain/Tickets/Templates/submodules/timesheet.blade.php
Normal 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>
|
||||
Reference in New Issue
Block a user