OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
249
app/Domain/Tickets/Templates/calendar.blade.php
Normal file
249
app/Domain/Tickets/Templates/calendar.blade.php
Normal file
@@ -0,0 +1,249 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$milestones = $milestones ?? [];
|
||||
if (! session()->exists('usersettings.submenuToggle.myProjectCalendarView')) {
|
||||
session(['usersettings.submenuToggle.myProjectCalendarView' => 'dayGridMonth']);
|
||||
}
|
||||
@endphp
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@include('tickets::submodules.timelineHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
@include('tickets::submodules.timelineTabs')
|
||||
<div class="maincontentinner">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@dispatchEvent('filters.afterLefthandSectionOpen')
|
||||
|
||||
@include('tickets::submodules.ticketNewBtn')
|
||||
@include('tickets::submodules.ticketFilter')
|
||||
|
||||
@dispatchEvent('filters.beforeLefthandSectionClose')
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="fc-center center" id="calendarTitle" style="padding-top:5px;">
|
||||
<h2>..</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
|
||||
<button class="fc-next-button btn btn-default right" type="button" style="margin-right:5px;">
|
||||
<span class="fc-icon fc-icon-chevron-right"></span>
|
||||
</button>
|
||||
<button class="fc-prev-button btn btn-default right" type="button" style="margin-right:5px;">
|
||||
<span class="fc-icon fc-icon-chevron-left"></span>
|
||||
</button>
|
||||
|
||||
<button class="fc-today-button btn btn-default right" style="margin-right:5px;">today</button>
|
||||
|
||||
|
||||
<select id="my-select" style="margin-right:5px;" class="right">
|
||||
<option class="fc-timeGridDay-button fc-button fc-state-default fc-corner-right" value="timeGridDay" {{ session('usersettings.submenuToggle.myProjectCalendarView') == 'timeGridDay' ? 'selected' : '' }}>Day</option>
|
||||
<option class="fc-timeGridWeek-button fc-button fc-state-default fc-corner-right" value="timeGridWeek" {{ session('usersettings.submenuToggle.myProjectCalendarView') == 'timeGridWeek' ? 'selected' : '' }}>Week</option>
|
||||
<option class="fc-dayGridMonth-button fc-button fc-state-default fc-corner-right" value="dayGridMonth" {{ session('usersettings.submenuToggle.myProjectCalendarView') == 'dayGridMonth' ? 'selected' : '' }}>Month</option>
|
||||
<option class="fc-multiMonthYear-button fc-button fc-state-default fc-corner-right" value="multiMonthYear" {{ session('usersettings.submenuToggle.myProjectCalendarView') == 'multiMonthYear' ? 'selected' : '' }}>Year</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="calendar-wrapper">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
|
||||
@if (isset($_GET['showMilestoneModal']))
|
||||
@php
|
||||
$modalUrl = $_GET['showMilestoneModal'] == '' ? '' : '/'.(int) $_GET['showMilestoneModal'];
|
||||
@endphp
|
||||
|
||||
leantime.ticketsController.openMilestoneModalManually("{{ BASE_URL }}/tickets/editMilestone{{ $modalUrl }}");
|
||||
window.history.pushState({},document.title, '{{ BASE_URL }}/tickets/roadmap');
|
||||
|
||||
@endif
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
var events = [
|
||||
@foreach ($milestones as $mlst)
|
||||
@php
|
||||
$headline = __('label.'.strtolower($mlst->type)).': '.$mlst->headline;
|
||||
if ($mlst->type == 'milestone') {
|
||||
$headline .= ' ('.$mlst->percentDone.'% Done)';
|
||||
}
|
||||
|
||||
$color = '#8D99A6';
|
||||
if ($mlst->type == 'milestone') {
|
||||
$color = $mlst->tags;
|
||||
}
|
||||
|
||||
$sortIndex = 0;
|
||||
if ($mlst->sortIndex != '' && is_numeric($mlst->sortIndex)) {
|
||||
$sortIndex = $mlst->sortIndex;
|
||||
}
|
||||
|
||||
$dependencyList = [];
|
||||
if ($mlst->milestoneid != 0) {
|
||||
$dependencyList[] = $mlst->milestoneid;
|
||||
}
|
||||
|
||||
if ($mlst->dependingTicketId != 0) {
|
||||
$dependencyList[] = $mlst->dependingTicketId;
|
||||
}
|
||||
@endphp
|
||||
|
||||
{
|
||||
|
||||
title: @json($headline),
|
||||
|
||||
@if (dtHelper()->isValidDateString($mlst->dateToFinish))
|
||||
start: new Date({{ format($mlst->dateToFinish)->jsTimestamp() }}),
|
||||
end: new Date({{ format(dtHelper()->parseDbDateTime($mlst->dateToFinish)->addHour(1))->jsTimestamp() }}),
|
||||
@elseif (dtHelper()->isValidDateString($mlst->editFrom))
|
||||
start: new Date({{ format($mlst->editFrom)->jsTimestamp() }}),
|
||||
end: new Date({{ format($mlst->editTo)->jsTimestamp() }}),
|
||||
@endif
|
||||
|
||||
|
||||
enitityId: {{ $mlst->id }},
|
||||
@if ($mlst->type == 'milestone')
|
||||
url: '#/tickets/editMilestone/{{ $mlst->id }}',
|
||||
color: '{{ $color }}',
|
||||
enitityType: "milestone",
|
||||
allDay: true,
|
||||
@else
|
||||
url: '#/tickets/showTicket/{{ $mlst->id }}',
|
||||
color: '{{ $color }}',
|
||||
enitityType: "ticket",
|
||||
allDay: false,
|
||||
@endif
|
||||
|
||||
},
|
||||
@endforeach
|
||||
];
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const heightWindow = jQuery("body").height() - 190;
|
||||
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
|
||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
timeZone: leantime.i18n.__("usersettings.timezone"),
|
||||
height:heightWindow,
|
||||
initialView: '{{ session('usersettings.submenuToggle.myProjectCalendarView') }}',
|
||||
events: events,
|
||||
editable: true,
|
||||
headerToolbar: false,
|
||||
dayHeaderFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "luxon"),
|
||||
eventTimeFormat: leantime.dateHelper.getFormatFromSettings("timeformat", "luxon"),
|
||||
slotLabelFormat: leantime.dateHelper.getFormatFromSettings("timeformat", "luxon"),
|
||||
views: {
|
||||
timeGridDay: {
|
||||
|
||||
},
|
||||
timeGridWeek: {
|
||||
|
||||
},
|
||||
dayGridMonth: {
|
||||
dayHeaderFormat: { weekday: 'short' },
|
||||
},
|
||||
multiMonthYear: {
|
||||
showNonCurrentDates: true,
|
||||
multiMonthTitleFormat: { month: 'long', year: 'numeric' },
|
||||
dayHeaderFormat: { weekday: 'short' },
|
||||
}
|
||||
},
|
||||
nowIndicator: true,
|
||||
bootstrapFontAwesome: {
|
||||
close: 'fa-times',
|
||||
prev: 'fa-chevron-left',
|
||||
next: 'fa-chevron-right',
|
||||
prevYear: 'fa-angle-double-left',
|
||||
nextYear: 'fa-angle-double-right'
|
||||
},
|
||||
eventDrop: function (event) {
|
||||
|
||||
leantime.rpc('Tickets.Tickets.patchTicket', {
|
||||
id: event.event.extendedProps.enitityId,
|
||||
values: {
|
||||
editFrom: event.event.startStr,
|
||||
editTo: event.event.endStr
|
||||
}
|
||||
}).catch(function (error) {
|
||||
jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" });
|
||||
event.revert();
|
||||
console.error('Could not update ticket dates', error);
|
||||
});
|
||||
},
|
||||
eventResize: function (event) {
|
||||
|
||||
leantime.rpc('Tickets.Tickets.patchTicket', {
|
||||
id: event.event.extendedProps.enitityId,
|
||||
values: {
|
||||
editFrom: event.event.startStr,
|
||||
editTo: event.event.endStr
|
||||
}
|
||||
}).catch(function (error) {
|
||||
jQuery.growl({ message: (error && error.message) ? error.message : leantime.i18n.__("short_notifications.not_saved"), style: "error" });
|
||||
event.revert();
|
||||
console.error('Could not update ticket dates', error);
|
||||
})
|
||||
|
||||
},
|
||||
eventMouseEnter: function() {
|
||||
}
|
||||
}
|
||||
);
|
||||
calendar.setOption('locale', leantime.i18n.__("language.code"));
|
||||
calendar.render();
|
||||
calendar.scrollToTime( 100 );
|
||||
jQuery("#calendarTitle h2").text(calendar.getCurrentData().viewTitle);
|
||||
|
||||
jQuery('.fc-prev-button').click(function() {
|
||||
calendar.prev();
|
||||
calendar.getCurrentData()
|
||||
jQuery("#calendarTitle h2").text(calendar.getCurrentData().viewTitle);
|
||||
});
|
||||
jQuery('.fc-next-button').click(function() {
|
||||
calendar.next();
|
||||
jQuery("#calendarTitle h2").text(calendar.getCurrentData().viewTitle);
|
||||
});
|
||||
jQuery('.fc-today-button').click(function() {
|
||||
calendar.today();
|
||||
jQuery("#calendarTitle h2").text(calendar.getCurrentData().viewTitle);
|
||||
});
|
||||
jQuery("#my-select").on("change", function(e){
|
||||
|
||||
calendar.changeView(jQuery("#my-select option:selected").val());
|
||||
|
||||
leantime.rpc('Api.Api.setSubmenuState', {
|
||||
submenu: "myProjectCalendarView",
|
||||
state: jQuery("#my-select option:selected").val()
|
||||
}).catch(function (e) { console.error('Could not update submenu state', e); });
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
459
app/Domain/Tickets/Templates/componentTest.blade.php
Normal file
459
app/Domain/Tickets/Templates/componentTest.blade.php
Normal file
@@ -0,0 +1,459 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Swimlane Component Test</title>
|
||||
<link rel="stylesheet" href="{{ BASE_URL }}/dist/css/main.3.5.12.min.css">
|
||||
<style>
|
||||
body { padding: 20px; background: #f5f5f5; }
|
||||
.test-section { background: white; padding: 20px; margin-bottom: 20px; border-radius: 8px; }
|
||||
.test-section h2 { margin-top: 0; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||
.component-demo { margin: 15px 0; padding: 15px; background: #f9f9f9; border-radius: 4px; }
|
||||
.component-label { font-weight: bold; color: #666; margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Swimlane Row Header Components Test</h1>
|
||||
<p>Testing all new Blade components before integration</p>
|
||||
|
||||
<!-- Icon Components Test -->
|
||||
<div class="test-section">
|
||||
<h2>1. Icon Components</h2>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">ThermometerIcon (Priority)</div>
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<div>
|
||||
<small>Critical (1):</small>
|
||||
<x-global::kanban.thermometer-icon :priority="1" />
|
||||
</div>
|
||||
<div>
|
||||
<small>High (2):</small>
|
||||
<x-global::kanban.thermometer-icon :priority="2" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Medium (3):</small>
|
||||
<x-global::kanban.thermometer-icon :priority="3" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Low (4):</small>
|
||||
<x-global::kanban.thermometer-icon :priority="4" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Lowest (5):</small>
|
||||
<x-global::kanban.thermometer-icon :priority="5" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<small>With labels:</small><br>
|
||||
<x-global::kanban.thermometer-icon :priority="1" :showLabel="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">TShirtIcon (Effort)</div>
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<x-global::kanban.tshirt-icon :effort="1" :showLabel="true" />
|
||||
<x-global::kanban.tshirt-icon :effort="2" :showLabel="true" />
|
||||
<x-global::kanban.tshirt-icon :effort="3" :showLabel="true" />
|
||||
<x-global::kanban.tshirt-icon :effort="5" :showLabel="true" />
|
||||
<x-global::kanban.tshirt-icon :effort="8" :showLabel="true" />
|
||||
<x-global::kanban.tshirt-icon :effort="13" :showLabel="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">UserAvatar (with consistent color generation)</div>
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<div>
|
||||
<small>Marcus Wells (SM):</small><br>
|
||||
<x-global::kanban.user-avatar username="Marcus Wells" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Sarah Connor (MD):</small><br>
|
||||
<x-global::kanban.user-avatar username="Sarah Connor" size="md" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Unassigned (MD):</small><br>
|
||||
<x-global::kanban.user-avatar username="Unassigned" size="md" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Bob Johnson (LG):</small><br>
|
||||
<x-global::kanban.user-avatar username="Bob Johnson" size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px; font-size: 12px; color: #666;">
|
||||
Note: Each username gets a consistent color. Same user = same color every time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">TimeIndicator</div>
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<div>
|
||||
<small>Due Soon:</small>
|
||||
<x-global::kanban.time-indicator type="dueSoon" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Overdue:</small>
|
||||
<x-global::kanban.time-indicator type="overdue" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Stale:</small>
|
||||
<x-global::kanban.time-indicator type="stale" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Supporting Icons</div>
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<div>
|
||||
<small>Milestone 🎯:</small>
|
||||
<x-global::kanban.milestone-icon label="Sprint 1" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Bug 🐛:</small>
|
||||
<x-global::kanban.type-icon type="bug" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Feature ✨:</small>
|
||||
<x-global::kanban.type-icon type="feature" />
|
||||
</div>
|
||||
<div>
|
||||
<small>Sprint 🏃:</small>
|
||||
<x-global::kanban.sprint-icon label="Sprint 2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MicroProgressBar Test -->
|
||||
<div class="test-section">
|
||||
<h2>2. MicroProgressBar Component</h2>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Status Breakdown (50% New, 30% In Progress, 20% Done)</div>
|
||||
<x-global::kanban.micro-progress-bar
|
||||
:statusCounts="['3' => 5, '4' => 3, '5' => 2]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:totalCount="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Different Distribution (70% New, 20% In Progress, 10% Done)</div>
|
||||
<x-global::kanban.micro-progress-bar
|
||||
:statusCounts="['3' => 14, '4' => 4, '5' => 2]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:totalCount="20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">All Statuses</div>
|
||||
<x-global::kanban.micro-progress-bar
|
||||
:statusCounts="['1' => 2, '3' => 5, '4' => 3, '2' => 1, '5' => 4]"
|
||||
:statusColumns="['1' => 'Blocked', '3' => 'New', '4' => 'In Progress', '2' => 'Waiting', '5' => 'Done']"
|
||||
:totalCount="15"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Count Badge Variations -->
|
||||
<div class="test-section">
|
||||
<h2>3. CountBadge Color Variations</h2>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Small Count (1-9) - Light Olive</div>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="priority"
|
||||
:groupId="3"
|
||||
label="Medium"
|
||||
:totalCount="5"
|
||||
:statusCounts="['3' => 3, '4' => 1, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Medium Count (10-99) - Medium Olive</div>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="priority"
|
||||
:groupId="2"
|
||||
label="High"
|
||||
:totalCount="12"
|
||||
:statusCounts="['3' => 5, '4' => 4, '5' => 3]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Large Count (100+) - Dark Olive</div>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="priority"
|
||||
:groupId="1"
|
||||
label="Critical"
|
||||
:totalCount="128"
|
||||
:statusCounts="['3' => 50, '4' => 48, '5' => 30]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SwimLaneRowHeader Test -->
|
||||
<div class="test-section">
|
||||
<h2>4. Complete SwimLaneRowHeader Components (Matching Design)</h2>
|
||||
|
||||
<h3 style="color: #6B7A4D; margin-top: 20px;">EXPANDED vs COLLAPSED States</h3>
|
||||
<div style="display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 30px;">
|
||||
<div>
|
||||
<small style="color: #666;">Expanded (shows progress bar)</small>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="priority"
|
||||
:groupId="1"
|
||||
label="Critical"
|
||||
:totalCount="4"
|
||||
:statusCounts="['3' => 2, '4' => 1, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
timeAlert="overdue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<small style="color: #666;">Collapsed (minimal)</small>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="priority"
|
||||
:groupId="2"
|
||||
label="High"
|
||||
:totalCount="6"
|
||||
:statusCounts="['3' => 3, '4' => 2, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="false"
|
||||
timeAlert="dueSoon"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<small style="color: #666;">Collapsed (USER groupby)</small>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="editorId"
|
||||
groupId="123"
|
||||
label="Sarah Chen"
|
||||
:totalCount="5"
|
||||
:statusCounts="['3' => 2, '4' => 2, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="false"
|
||||
timeAlert="overdue"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<small style="color: #666;">Collapsed (EFFORT groupby)</small>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="storypoints"
|
||||
:groupId="5"
|
||||
label="L"
|
||||
:totalCount="3"
|
||||
:statusCounts="['3' => 1, '4' => 1, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="false"
|
||||
timeAlert="stale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #6B7A4D; margin-top: 20px;">GROUP BY: EFFORT</h3>
|
||||
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="storypoints"
|
||||
:groupId="1"
|
||||
label="XS"
|
||||
:totalCount="5"
|
||||
:statusCounts="['3' => 2, '4' => 2, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="storypoints"
|
||||
:groupId="2"
|
||||
label="S"
|
||||
:totalCount="18"
|
||||
:statusCounts="['3' => 6, '4' => 6, '5' => 6]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
timeAlert="dueSoon"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="storypoints"
|
||||
:groupId="3"
|
||||
label="M"
|
||||
:totalCount="6"
|
||||
:statusCounts="['3' => 2, '4' => 2, '5' => 2]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="storypoints"
|
||||
:groupId="5"
|
||||
label="L"
|
||||
:totalCount="3"
|
||||
:statusCounts="['3' => 0, '4' => 1, '5' => 2]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
timeAlert="overdue"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="storypoints"
|
||||
:groupId="8"
|
||||
label="XL"
|
||||
:totalCount="2"
|
||||
:statusCounts="['3' => 1, '4' => 1, '5' => 0]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #6B7A4D; margin-top: 20px;">GROUP BY: MILESTONE</h3>
|
||||
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="milestoneid"
|
||||
groupId="1"
|
||||
label="Q1 Launch"
|
||||
:totalCount="12"
|
||||
:statusCounts="['3' => 4, '4' => 5, '5' => 3]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
timeAlert="dueSoon"
|
||||
moreInfo="Start: Jan 1, 2024 • End: Mar 31, 2024 • Status: Active"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="milestoneid"
|
||||
groupId="2"
|
||||
label="Beta Release"
|
||||
:totalCount="8"
|
||||
:statusCounts="['3' => 2, '4' => 2, '5' => 4]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
moreInfo="Start: Feb 1, 2024 • End: Feb 28, 2024 • Status: Completed"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="milestoneid"
|
||||
groupId="3"
|
||||
label="User Research"
|
||||
:totalCount="5"
|
||||
:statusCounts="['3' => 3, '4' => 2, '5' => 0]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
moreInfo="Start: Mar 1, 2024 • End: Apr 15, 2024 • Status: In Progress"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="milestoneid"
|
||||
groupId="0"
|
||||
label="No Milestone"
|
||||
:totalCount="7"
|
||||
:statusCounts="['3' => 4, '4' => 2, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
timeAlert="stale"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #6B7A4D; margin-top: 20px;">GROUP BY: SPRINT</h3>
|
||||
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="sprint"
|
||||
groupId="23"
|
||||
label="Sprint 23"
|
||||
:totalCount="15"
|
||||
:statusCounts="['3' => 5, '4' => 7, '5' => 3]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
timeAlert="dueSoon"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="sprint"
|
||||
groupId="24"
|
||||
label="Sprint 24"
|
||||
:totalCount="8"
|
||||
:statusCounts="['3' => 3, '4' => 4, '5' => 1]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
<x-global::kanban.swimlane-row-header
|
||||
groupBy="sprint"
|
||||
groupId="0"
|
||||
label="Backlog"
|
||||
:totalCount="24"
|
||||
:statusCounts="['3' => 18, '4' => 4, '5' => 2]"
|
||||
:statusColumns="['3' => 'New', '4' => 'In Progress', '5' => 'Done']"
|
||||
:expanded="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding: 15px; background: #FFF9E6; border-left: 4px solid #F5A623; border-radius: 4px;">
|
||||
<strong>💡 Design Notes:</strong>
|
||||
<ul style="margin: 8px 0; padding-left: 20px;">
|
||||
<li><strong>Two states only:</strong> Expanded (shows progress bar) and Collapsed (hides progress bar)</li>
|
||||
<li><strong>Label text always visible</strong> in both states - only progress bar toggles</li>
|
||||
<li><strong>Count badge styling differs by state:</strong>
|
||||
<ul style="margin: 4px 0; padding-left: 20px;">
|
||||
<li>Expanded: Olive green background badge</li>
|
||||
<li>Collapsed: Plain number (no background)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Chevron rotates: ▼ (expanded) → ► (collapsed)</li>
|
||||
<li>Better spacing and alignment in both states</li>
|
||||
<li>Time indicators (⏳⏰💤) inline with label row</li>
|
||||
<li>Milestone dates show on hover only (not displayed on card)</li>
|
||||
<li>Icons match design: 🎯 for milestones, 🏃 for sprints, 👕 for effort</li>
|
||||
<li>Smooth transition animation between states (0.2s ease)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>5. Design Tokens (CSS Variables)</h2>
|
||||
<div class="component-demo">
|
||||
<div class="component-label">Priority Colors</div>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<div style="width: 120px; padding: 10px; background: var(--priority-critical); color: white; border-radius: 4px;">Critical</div>
|
||||
<div style="width: 120px; padding: 10px; background: var(--priority-high); color: white; border-radius: 4px;">High</div>
|
||||
<div style="width: 120px; padding: 10px; background: var(--priority-medium); color: white; border-radius: 4px;">Medium</div>
|
||||
<div style="width: 120px; padding: 10px; background: var(--priority-low); color: white; border-radius: 4px;">Low</div>
|
||||
<div style="width: 120px; padding: 10px; background: var(--priority-lowest); color: white; border-radius: 4px;">Lowest</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('Component test page loaded');
|
||||
|
||||
// Implement toggle functionality for test page
|
||||
window.leantime = window.leantime || {};
|
||||
window.leantime.kanbanController = window.leantime.kanbanController || {};
|
||||
|
||||
window.leantime.kanbanController.toggleSwimlane = function(id) {
|
||||
console.log('Toggle swimlane:', id);
|
||||
|
||||
const header = document.querySelector('[data-swimlane-id="' + id + '"]');
|
||||
if (!header) return;
|
||||
|
||||
const isExpanded = header.getAttribute('data-expanded') === 'true';
|
||||
const newExpanded = !isExpanded;
|
||||
|
||||
// Update data attribute
|
||||
header.setAttribute('data-expanded', newExpanded.toString());
|
||||
|
||||
// Find the component and re-render it (in real app, this would be handled by HTMX/server)
|
||||
// For test page, we'll use a simple approach: reload the page or toggle classes
|
||||
// Since we can't easily re-render Blade, let's just show a message
|
||||
alert('Toggle clicked! In the real Kanban view, this will expand/collapse the swimlane.\n\nCurrent state: ' + (isExpanded ? 'Expanded' : 'Collapsed') + '\nNew state: ' + (newExpanded ? 'Expanded' : 'Collapsed'));
|
||||
|
||||
// In Phase 7, we'll implement actual server-side toggle with HTMX or page reload
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
7
app/Domain/Tickets/Templates/delMilestone.blade.php
Normal file
7
app/Domain/Tickets/Templates/delMilestone.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<h4 class="widgettitle title-light">{!! __('subtitles.delete_milestone') !!}</h4>
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/tickets/delMilestone/{{ $ticket->id }}">
|
||||
<p>{!! __('text.confirm_milestone_deletion') !!}</p><br />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ BASE_URL }}/tickets/roadmap/">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</form>
|
||||
20
app/Domain/Tickets/Templates/delTicket.blade.php
Normal file
20
app/Domain/Tickets/Templates/delTicket.blade.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<h4 class="widgettitle title-light">{!! __('subtitles.delete') !!}</h4>
|
||||
|
||||
@if (!empty($error))
|
||||
{!! $error !!}
|
||||
@else
|
||||
|
||||
@if (is_object($ticket))
|
||||
<form method="post" action="{{ BASE_URL }}/tickets/delTicket/{{ $ticket->id }}">
|
||||
<p>{!! __('text.confirm_ticket_deletion') !!}</p><br />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
|
||||
|
||||
<x-global::forms.button tag="a" contentRole="tertiary" link="#/tickets/showTicket/{{ $ticket->id }}">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
@else
|
||||
<p>Ticket not found</p>
|
||||
@endif
|
||||
@endif
|
||||
133
app/Domain/Tickets/Templates/milestoneDialog.blade.php
Normal file
133
app/Domain/Tickets/Templates/milestoneDialog.blade.php
Normal file
@@ -0,0 +1,133 @@
|
||||
@php
|
||||
$currentMilestone = $milestone ?? null;
|
||||
$milestones = $milestones ?? [];
|
||||
$statusLabels = $statusLabels ?? [];
|
||||
@endphp
|
||||
|
||||
<script type="text/javascript">
|
||||
window.onload = function() {
|
||||
if (!window.jQuery) {
|
||||
//It's not a modal
|
||||
location.href="{{ BASE_URL }}/tickets/roadmap?showMilestoneModal={{ $currentMilestone->id }}";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="modal-icons">
|
||||
@if (isset($currentMilestone->id) && $currentMilestone->id != '')
|
||||
<a href="#/tickets/delMilestone/{{ $currentMilestone->id }}" class="danger" data-tippy-content="Delete"><i class='fa fa-trash-can'></i></a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h4 class="widgettitle title-light">{!! __('headline.milestone') !!} </h4>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form class="formModal" method="post" action="{{ BASE_URL }}/tickets/editMilestone/{{ $currentMilestone->id }}" style="min-width: 250px;">
|
||||
|
||||
<label>{!! __('label.milestone_title') !!}</label>
|
||||
<x-global::forms.text-input name="headline" value="{{ $currentMilestone->headline }}" placeholder="{{ __('label.milestone_title') }}" /><br />
|
||||
|
||||
<label class="control-label">{!! __('label.project') !!}</label>
|
||||
<select name="projectId" class="tw-w-full">
|
||||
@foreach ($allAssignedprojects as $project)
|
||||
@if (empty($project['type']) || $project['type'] == 'project')
|
||||
<option value="{{ $project['id'] }}"
|
||||
@if (!empty($currentMilestone->projectId) && $currentMilestone->projectId == $project['id'])
|
||||
selected
|
||||
@elseif (session('currentProject') == $project['id'])
|
||||
selected
|
||||
@endif
|
||||
>{{ $project['name'] }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<label>{!! __('label.todo_status') !!}</label>
|
||||
<select id="status-select" name="status" class="span11"
|
||||
data-placeholder="{{ isset($statusLabels[$currentMilestone->status]) ? $statusLabels[$currentMilestone->status]['name'] : '' }}">
|
||||
|
||||
@foreach ($statusLabels as $key => $label)
|
||||
<option value="{{ $key }}"
|
||||
@if ($currentMilestone->status == $key) selected='selected' @endif
|
||||
>{{ $label['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<label>{!! __('label.dependent_on') !!}</label>
|
||||
<select name="dependentMilestone" class="span11">
|
||||
<option value="">{!! __('label.no_dependency') !!}</option>
|
||||
@foreach ($milestones as $milestoneRow)
|
||||
@if ($milestoneRow->id !== $currentMilestone->id)
|
||||
<option value="{{ $milestoneRow->id }}"
|
||||
@if ($currentMilestone->milestoneid == $milestoneRow->id) selected='selected' @endif
|
||||
>{{ $milestoneRow->headline }} </option>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
</select>
|
||||
|
||||
<label>{!! __('label.owner') !!}</label>
|
||||
<select data-placeholder="{{ __('input.placeholders.filter_by_user') }}"
|
||||
name="editorId" class="user-select span11">
|
||||
<option value="">{!! __('dropdown.not_assigned') !!}</option>
|
||||
@foreach ($users as $userRow)
|
||||
<option value="{{ $userRow['id'] }}"
|
||||
@if ($currentMilestone->editorId == $userRow['id']) selected='selected' @endif
|
||||
>{{ $userRow['firstname'] }} {{ $userRow['lastname'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<label>{!! __('label.color') !!}</label>
|
||||
<input type="text" name="tags" autocomplete="off" value="{{ $currentMilestone->tags }}" placeholder="{{ __('input.placeholders.pick_a_color') }}" class="simpleColorPicker"/><br />
|
||||
|
||||
<label>{!! __('label.planned_start_date') !!}</label>
|
||||
<input type="text" name="editFrom" autocomplete="off" value="{{ format($currentMilestone->editFrom)->date() }}" placeholder="{{ __('language.dateformat') }}" id="milestoneEditFrom" /><br />
|
||||
|
||||
<label>{!! __('label.planned_end_date') !!}</label>
|
||||
<input type="text" name="editTo" autocomplete="off" value="{{ format($currentMilestone->editTo)->date() }}" placeholder="{{ __('language.dateformat') }}" id="milestoneEditTo" /><br />
|
||||
|
||||
<label>{!! __('label.outcome_impact') !!}</label>
|
||||
<textarea name="outcomeImpact" rows="3" class="tw-w-full"
|
||||
placeholder="{{ __('input.placeholders.outcome_impact') }}">{{ $currentMilestone->outcomeImpact }}</textarea><br />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" />
|
||||
</div>
|
||||
<div class="col-md-6 align-right padding-top-sm">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@if (isset($currentMilestone->id) && $currentMilestone->id !== '')
|
||||
<br />
|
||||
<input type="hidden" name="comment" value="1" />
|
||||
|
||||
@include('comments::submodules.generalComment', ['formUrl' => '/tickets/editMilestone/'.$currentMilestone->id])
|
||||
@endif
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
@if (isset($_GET['closeModal']))
|
||||
jQuery.nmTop().close();
|
||||
return;
|
||||
@endif
|
||||
|
||||
leantime.ticketsController.initSimpleColorPicker();
|
||||
leantime.ticketsController.initMilestoneDates();
|
||||
|
||||
@if (!$login::userIsAtLeast($roles::$editor))
|
||||
leantime.authController.makeInputReadonly(".nyroModalCont");
|
||||
@endif
|
||||
|
||||
@if ($login::userHasRole([$roles::$commenter]))
|
||||
leantime.commentsController.enableCommenterForms();
|
||||
@endif
|
||||
|
||||
|
||||
})
|
||||
</script>
|
||||
53
app/Domain/Tickets/Templates/moveTicket.blade.php
Normal file
53
app/Domain/Tickets/Templates/moveTicket.blade.php
Normal file
@@ -0,0 +1,53 @@
|
||||
@if ($ticket->type == 'milestone')
|
||||
<h4 class="widgettitle title-light">{!! __('headline.move_milestone') !!} </h4>
|
||||
@else
|
||||
<h4 class="widgettitle title-light">{!! __('headline.move_todo') !!} </h4>
|
||||
@endif
|
||||
|
||||
|
||||
<form method="post" action="{{ BASE_URL }}/tickets/moveTicket/{{ $ticket->id }}" class="formModal">
|
||||
<h3>#{{ $ticket->id }} - {{ $ticket->headline }}</h3> <br />
|
||||
<p>
|
||||
@if ($ticket->type == 'milestone')
|
||||
{!! __('text.moving_milestones') !!}
|
||||
@else
|
||||
{!! __('text.moving') !!}
|
||||
@endif
|
||||
|
||||
<br /><br />
|
||||
</p>
|
||||
|
||||
<select id="projectSelector" name="projectId">
|
||||
@php
|
||||
$i = 0;
|
||||
$lastClient = '';
|
||||
foreach ($projects as $projectRow) {
|
||||
if ($lastClient != $projectRow['clientName']) {
|
||||
$lastClient = $projectRow['clientName'];
|
||||
if ($i > 1) {
|
||||
echo '</optgroup>';
|
||||
}
|
||||
echo "<optgroup label='".$tpl->escape($projectRow['clientName'])."'> ";
|
||||
}
|
||||
echo "<option value='".$projectRow['id']."'>".$tpl->escape($projectRow['name']).'</option>';
|
||||
$i++;
|
||||
}
|
||||
@endphp
|
||||
</select><br /><br /><br /><br />
|
||||
<br />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.move')" name="move" />
|
||||
<x-global::forms.button tag="a" class="pull-right" link="javascript:void(0);" onclick="jQuery.nmTop().close();" contentRole="tertiary">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
<div class="clearall"></div>
|
||||
<br />
|
||||
</form>
|
||||
|
||||
|
||||
<script>
|
||||
@if (isset($_GET['closeModal']))
|
||||
jQuery.nmTop().close();
|
||||
@endif
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
jQuery("#projectSelector").chosen();
|
||||
});
|
||||
</script>
|
||||
57
app/Domain/Tickets/Templates/newTicket.blade.php
Normal file
57
app/Domain/Tickets/Templates/newTicket.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
|
||||
<div class="pull-right padding-top">
|
||||
<a href="{{ session('lastPage') }}" class="backBtn"><i class="far fa-arrow-alt-circle-left"></i> {!! __('links.go_back') !!}</a>
|
||||
</div>
|
||||
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{{ session('currentProjectClient').' // '.session('currentProjectName') }}</h5>
|
||||
<h1>{!! __('headlines.new_to_do') !!}</h1>
|
||||
</div>
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="tabbedwidget tab-primary ticketTabs">
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#ticketdetails">{!! __('tabs.ticketDetails') !!}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="ticketdetails">
|
||||
<form class="ticketModal" action="{{ BASE_URL }}/tickets/newTicket" method="post">
|
||||
@include('tickets::submodules.ticketDetails')
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
leantime.ticketsController.initTicketTabs();
|
||||
leantime.ticketsController.initTagsInput();
|
||||
});
|
||||
|
||||
|
||||
jQuery(window).load(function () {
|
||||
jQuery(window).resize();
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
64
app/Domain/Tickets/Templates/newTicketModal.blade.php
Normal file
64
app/Domain/Tickets/Templates/newTicketModal.blade.php
Normal file
@@ -0,0 +1,64 @@
|
||||
@php
|
||||
$projectData = $projectData ?? [];
|
||||
$todoTypeIcons = $ticketTypeIcons ?? [];
|
||||
@endphp
|
||||
|
||||
<div style="min-width:90%">
|
||||
<h1>{!! __('headlines.new_to_do') !!}</h1>
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="tabbedwidget tab-primary ticketTabs" style="visibility:hidden;">
|
||||
|
||||
<ul>
|
||||
<li><a href="#ticketdetails">{!! __('tabs.ticketDetails') !!}</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="ticketdetails">
|
||||
<form class="formModal" action="{{ BASE_URL }}/tickets/newTicket" method="post">
|
||||
@include('tickets::submodules.ticketDetails')
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
@if (isset($_GET['closeModal']))
|
||||
jQuery.nmTop().close();
|
||||
@endif
|
||||
|
||||
leantime.ticketsController.initTicketTabs();
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
|
||||
leantime.dateController.initDatePicker(".dates");
|
||||
leantime.dateController.initDateRangePicker(".editFrom", ".editTo");
|
||||
|
||||
leantime.ticketsController.initTagsInput();
|
||||
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
|
||||
jQuery(".ticketTabs select").chosen();
|
||||
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".nyroModalCont");
|
||||
|
||||
@endif
|
||||
|
||||
@if ($login::userHasRole([$roles::$commenter]))
|
||||
leantime.commentsController.enableCommenterForms();
|
||||
@endif
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
@props([
|
||||
'milestone' => null,
|
||||
'noText' => false,
|
||||
'percentDone' => 0,
|
||||
'progressColor' => 'default'
|
||||
])
|
||||
|
||||
<div class="ticketBox fixed">
|
||||
<div class="row">
|
||||
<div class="col-md-8" style="margin-bottom:5px;">
|
||||
<strong><a href="<?=BASE_URL ?>/tickets/showKanban?milestone={{ $milestone->id }}" >{{ $milestone->headline }}</a></strong>
|
||||
</div>
|
||||
<div class="col-md-4 align-right">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@fragment('progress')
|
||||
|
||||
@if($noText === false || $noText === null)
|
||||
<div class="row progress-wrapper">
|
||||
<div class="col-md-7 percent-label">
|
||||
{{ __("label.due") }}
|
||||
<?php echo format($milestone->editTo )->date($tpl->__("text.no_date_defined")); ?>
|
||||
</div>
|
||||
<div class="col-md-5 percent-label" style="text-align:right">
|
||||
<?=sprintf($tpl->__("text.percent_complete"), format($percentDone)->decimal())?>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="progress" data-tippy-content="<?=sprintf($tpl->__("text.percent_complete"), format($percentDone)->decimal())?>">
|
||||
<div class="progress-bar progress-bar-success"
|
||||
role="progressbar"
|
||||
aria-valuenow="{{ $percentDone }}"
|
||||
aria-valuemin="0" aria-valuemax="100"
|
||||
style="width: {{ $percentDone }}%; {{ $progressColor !== 'default' ? ' background: #'.$progressColor.'; ' : '' }}">
|
||||
<span class="sr-only">{{ sprintf($tpl->__("text.percent_complete"), format($percentDone)->decimal()) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Idempotent init — a bare tippy('[data-tippy-content]') here re-instanced
|
||||
// every tooltip on the page each time a milestone card rendered (flashing
|
||||
// + duplicate header tooltips). initTooltips skips already-instanced els.
|
||||
window.leantime?.initTooltips?.();
|
||||
</script>
|
||||
@endfragment
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
@php
|
||||
/**
|
||||
* Quick-add form partial for Kanban columns
|
||||
*
|
||||
* @var int $statusId - Status column ID
|
||||
* @var string|null $swimlaneKey - Swimlane identifier
|
||||
* @var bool $isEmpty - Whether column is empty
|
||||
* @var array|null $reopenState - Session flash data
|
||||
*/
|
||||
$isActive = !empty($reopenState)
|
||||
&& $reopenState['status'] == $statusId
|
||||
&& ($reopenState['swimlane'] ?? null) == ($swimlaneKey ?? null);
|
||||
|
||||
$savedHeadline = $isActive ? ($reopenState['headline'] ?? '') : '';
|
||||
$hasError = $isActive && !empty($reopenState['error']);
|
||||
@endphp
|
||||
|
||||
<div class="quickaddContainer tw-mb-s {{ $isEmpty ? 'quickaddContainer--empty' : '' }}" data-status="{{ $statusId }}" data-swimlane="{{ $swimlaneKey ?? '' }}">
|
||||
<a href="javascript:void(0);"
|
||||
class="quickAddLink {{ $isEmpty ? 'empty-state' : 'inline-add' }}"
|
||||
onclick="leantime.kanbanController.toggleQuickAdd(this)"
|
||||
aria-expanded="{{ $isActive ? 'true' : 'false' }}"
|
||||
aria-controls="quickadd-form-{{ $statusId }}-{{ $swimlaneKey ?? 'default' }}"
|
||||
style="{{ $isActive ? 'display:none;' : '' }}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>Add To-Do</span>
|
||||
</a>
|
||||
|
||||
<form method="post"
|
||||
class="quickAddForm {{ $isActive ? 'active' : '' }}"
|
||||
id="quickadd-form-{{ $statusId }}-{{ $swimlaneKey ?? 'default' }}"
|
||||
data-quickadd-form
|
||||
style="{{ $isActive ? '' : 'display:none;' }}"
|
||||
data-submitting="false">
|
||||
<input type="hidden" name="quickadd" value="1" />
|
||||
<input type="hidden" name="status" value="{{ $statusId }}" />
|
||||
<input type="hidden" name="swimlane" value="{{ $swimlaneKey ?? '' }}" />
|
||||
<input type="hidden" name="groupBy" value="{{ $currentGroupBy ?? '' }}" />
|
||||
<input type="hidden" name="milestone" value="{{ $searchCriteria['milestone'] ?? '' }}" />
|
||||
<input type="hidden" name="sprint" value="{{ session('currentSprint') ?? '' }}" />
|
||||
<input type="hidden" name="stay_open" value="0" data-stay-open-input />
|
||||
|
||||
@if (! empty($programBoard) && ! empty($availableProjects))
|
||||
{{-- Program board: a new task must belong to exactly one child project. --}}
|
||||
<div class="form-group">
|
||||
<select name="quickaddProjectId" class="form-control" required aria-label="{{ __('label.project') }}">
|
||||
<option value="">{{ __('label.project') }}…</option>
|
||||
@foreach ($availableProjects as $quickAddProjectId => $quickAddProjectName)
|
||||
<option value="{{ $quickAddProjectId }}">{{ $tpl->escape($quickAddProjectName) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label for="headline-{{ $statusId }}-{{ $swimlaneKey ?? 'default' }}" class="sr-only">Task name</label>
|
||||
<input type="text"
|
||||
name="headline"
|
||||
id="headline-{{ $statusId }}-{{ $swimlaneKey ?? 'default' }}"
|
||||
class="form-control quickAddInput {{ $hasError ? 'error' : '' }}"
|
||||
placeholder="What are you working on? ↵"
|
||||
value="{{ htmlspecialchars($savedHeadline) }}"
|
||||
{{ $isActive ? 'autofocus' : '' }}
|
||||
data-quickadd-input />
|
||||
|
||||
@if ($hasError)
|
||||
<div class="error-message" role="alert">{{ htmlspecialchars($reopenState['error']) }}</div>
|
||||
@endif
|
||||
|
||||
<div id="quick-add-help-{{ $statusId }}-{{ $swimlaneKey ?? 'default' }}" class="sr-only">
|
||||
Press Enter to save and close. Press Shift plus Enter to save and add another task. Press Escape to cancel.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="formButtonContainer">
|
||||
<x-global::forms.button inputType="submit" contentRole="primary" onclick="this.closest('form').dataset.submitting = 'true'; this.closest('form').querySelector('[data-stay-open-input]').value = '0';">Save</x-global::forms.button>
|
||||
<x-global::forms.button inputType="button" contentRole="secondary"
|
||||
onclick="leantime.kanbanController.toggleQuickAdd(this.closest('.quickaddContainer').querySelector('.quickAddLink'))">
|
||||
Cancel
|
||||
</x-global::forms.button>
|
||||
<i class="fa fa-circle-question"
|
||||
data-tippy-content="<strong>Keyboard Shortcuts:</strong><br>Enter: Save and close<br>Shift+Enter: Save and add another<br>Esc: Cancel"
|
||||
tabindex="0"
|
||||
aria-label="Keyboard shortcuts help"></i>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
163
app/Domain/Tickets/Templates/partials/subtasks.blade.php
Normal file
163
app/Domain/Tickets/Templates/partials/subtasks.blade.php
Normal file
@@ -0,0 +1,163 @@
|
||||
|
||||
<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.add_task") }}</a>
|
||||
<div class="ticketBox hideOnLoad" id="subticket_new" >
|
||||
|
||||
<form method="post" class="form-group"
|
||||
hx-post="{{ BASE_URL }}/tickets/subtasks/save?ticketId={{ $ticket->id }}"
|
||||
hx-indicator=".htmx-indicator-small"
|
||||
hx-target="#ticketSubtasks">
|
||||
<input type="hidden" value="new" name="subtaskId" />
|
||||
<input type="hidden" value="1" name="subtaskSave" />
|
||||
<input name="headline" type="text" title="{{ __("label.headline") }}" style="width:100%" placeholder="{{ __("input.placeholders.what_are_you_working_on") }}" />
|
||||
<input type="submit" value="{{ __("buttons.save") }}" name="quickadd" />
|
||||
<div class="htmx-indicator-small">
|
||||
<x-global::loader id="loadingthis" size="25px" />
|
||||
</div>
|
||||
<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 ($ticketSubtasks 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 = $tpl->__("text.anytime");
|
||||
} else {
|
||||
$date = format($subticket['dateToFinish'])->date();
|
||||
}
|
||||
|
||||
@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="javascript:void(0);" hx-delete="{{ BASE_URL }}/tickets/subtasks/delete?ticketId={{ $subticket["id"] }}&parentTicket={{ $ticket->id }}" hx-target="#ticketSubtasks" class="delete"><i class="fa fa-trash"></i> {{ __("links.delete_todo") }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<a href="#/tickets/showTicket/{{ $subticket['id'] }}">{{ $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 && isset($efforts[$subticket['storypoints']]))
|
||||
{{ $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='{{ $label["name"] }}' data-value='{{ $subticket['id'] }}_{{ $key }}_{{ $label["class"] }}' id='ticketStatusChange{{ $subticket['id'] . $key }}' >{{ $label["name"] }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function(){
|
||||
<?php if ($login::userIsAtLeast($roles::$editor)) { ?>
|
||||
|
||||
leantime.ticketsController.initAsyncInputChange();
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
|
||||
<?php } else { ?>
|
||||
|
||||
leantime.authController.makeInputReadonly(".nyroModalCont");
|
||||
|
||||
<?php } ?>
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
139
app/Domain/Tickets/Templates/partials/ticketCard.blade.php
Normal file
139
app/Domain/Tickets/Templates/partials/ticketCard.blade.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<div class="ticketBox fixed priority-border-{{ $row['priority'] }}" data-val="{{ $row['id'] }}">
|
||||
<div class="row">
|
||||
<div class="col-md-8 titleContainer">
|
||||
@if($cardType == "full")
|
||||
<small>{{ $row['projectName'] }}</small><br />
|
||||
@if($row['dependingTicketId'] > 0)
|
||||
<a href="#/tickets/showTicket/{{ $row['dependingTicketId'] }}">{{ $row['parentHeadline'] }}</a> //
|
||||
@endif
|
||||
@endif
|
||||
<strong><a href="#/tickets/showTicket/{{ $row['id'] }}" >{{ $row['headline'] }}</a></strong>
|
||||
|
||||
</div>
|
||||
<div class="col-md-4 timerContainer" style="padding:5px 15px;" id="timerContainer-{{ $row['id'] }}">
|
||||
|
||||
@include("tickets::partials.ticketsubmenu", ["ticket" => $row, "onTheClock" => $onTheClock])
|
||||
@if($cardType == "full")
|
||||
<div class="scheduler pull-right">
|
||||
@if( $row['editFrom'] != "0000-00-00 00:00:00" && $row['editFrom'] != "1969-12-31 00:00:00")
|
||||
<i class="fa-solid fa-calendar-check infoIcon tw-mr-xs" style="color:var(--accent2)" data-tippy-content="{{ __('text.schedule_to_start_on') }} {{ format($row['editFrom'])->date() }}"></i>
|
||||
@else
|
||||
<i class="fa-regular fa-calendar-xmark infoIcon tw-mr-xs" data-tippy-content="{{ __('text.not_scheduled_drag_ai') }}"></i>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-4" style="padding:0 15px;">
|
||||
@if($cardType == "full")
|
||||
<i class="fa-solid fa-business-time infoIcon" data-tippy-content=" {{ __("label.due") }}"></i>
|
||||
<input type="text" title="{{ __("label.due") }}" value="{{ format($row['dateToFinish'])->date(__("text.anytime")) }}" class="duedates secretInput" style="margin-left:0px;" data-id="{{ $row['id'] }}" name="date" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 dropdownContainer" style="padding-top:5px;">
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show right ">
|
||||
<a class="dropdown-toggle f-left status {{ $statusLabels[$row['status']]["class"] }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">
|
||||
@if(isset($statusLabels[$row['status']]))
|
||||
{{ $statusLabels[$row['status']]["name"] }}
|
||||
@else
|
||||
unknown
|
||||
@endif
|
||||
</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right" aria-labelledby="statusDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{{ __("dropdown.choose_status") }}</li>
|
||||
|
||||
@foreach ($statusLabels as $key => $label)
|
||||
<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);'
|
||||
class='{{ $label["class"] }}'
|
||||
data-label='{{ $label["name"] }}'
|
||||
data-value='{{ $row['id'] }}_{{ $key }}_{{ $label["class"] }}'
|
||||
id='ticketStatusChange{{$row['id'] . $key }}'>
|
||||
{{ $label["name"] }}
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php /*
|
||||
<div class="dropdown ticketDropdown effortDropdown show right">
|
||||
<a class="dropdown-toggle f-left label-default effort" href="javascript:void(0);" role="button" id="effortDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">
|
||||
@if ($row['storypoints'] != '' && $row['storypoints'] > 0)
|
||||
{{ $efforts["" . $row['storypoints']] ?? $row['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{{ $row['id'] }}">
|
||||
<li class="nav-header border">{{ __("dropdown.how_big_todo") }}</li>
|
||||
@foreach($efforts as $effortKey => $effortValue)
|
||||
<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);'
|
||||
data-value='{{ $row['id'] . "_" . $effortKey }}'
|
||||
id='ticketEffortChange{{ $row['id'] . $effortKey }}'>
|
||||
{{ $effortValue }}
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
*/ ?>
|
||||
@if($cardType == "full")
|
||||
<div class="dropdown ticketDropdown milestoneDropdown colorized show right tw-mr-sm">
|
||||
<a style="background-color:{{ $row['milestoneColor'] }}"
|
||||
class="dropdown-toggle f-left label-default milestone"
|
||||
href="javascript:void(0);"
|
||||
role="button" id="milestoneDropdownMenuLink{{ $row['id'] }}"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<span class="text">
|
||||
@if($row['milestoneid'] != "" && $row['milestoneid'] != 0)
|
||||
{{ $row['milestoneHeadline'] }}
|
||||
@else
|
||||
{{ __("label.no_milestone") }}
|
||||
@endif
|
||||
</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right" aria-labelledby="milestoneDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{{ __("dropdown.choose_milestone") }}</li>
|
||||
<li class='dropdown-item'>
|
||||
<a style='background-color:#b0b0b0'
|
||||
href='javascript:void(0);'
|
||||
data-label="{{__("label.no_milestone") }}"
|
||||
data-value='{{ $row['id'] }}_0_#b0b0b0'>
|
||||
{{ __("label.no_milestone") }}
|
||||
</a>
|
||||
</li>
|
||||
@if(isset($milestones))
|
||||
@foreach($milestones as $milestone)
|
||||
@if(is_object($milestone))
|
||||
<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);'
|
||||
data-label='{{ $milestone->headline }}'
|
||||
data-value='{{ $row['id'] }}_{{ $milestone->id }}_{{ $milestone->tags }}'
|
||||
id='ticketMilestoneChange{{ $row['id'] . $milestone->id }}'
|
||||
style='background-color:{{ $milestone->tags }}'>
|
||||
{{ $milestone->headline }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
@props([
|
||||
'ticket' => false,
|
||||
'onTheClock' => false,
|
||||
'allowSubtaskCreation' => false
|
||||
])
|
||||
|
||||
@if ($login::userIsAtLeast(\Leantime\Domain\Auth\Models\Roles::$editor))
|
||||
|
||||
<div class="inlineDropDownContainer" 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.todo") }}</li>
|
||||
@dispatchEvent("beforeShowTicket", ["ticket"=>$ticket])
|
||||
<li><a href="#/tickets/showTicket/{{ $ticket["id"] }}" class=''><i class="fa fa-edit"></i> {{ __("links.edit_todo") }}</a></li>
|
||||
@dispatchEvent("beforeMoveTicket", ["ticket"=>$ticket])
|
||||
<li><a href="#/tickets/moveTicket/{{ $ticket["id"] }}" class=""><i class="fa-solid fa-arrow-right-arrow-left"></i> {{ __("links.move_todo") }}</a></li>
|
||||
@if($allowSubtaskCreation)
|
||||
<li><a href="javascript:void(0);" onclick="jQuery('#subtask-form-{{$ticket['id']}}').toggle();"
|
||||
class="add-subtask-link">
|
||||
<i class="fa-solid fa-diagram-predecessor"></i> Add Subtask</a></li>
|
||||
@endif
|
||||
@dispatchEvent("beforeDeleteTicket", ["ticket"=>$ticket])
|
||||
<li><a href="#/tickets/delTicket/{{ $ticket["id"] }}" class="delete"><i class="fa fa-trash"></i> {{ __("links.delete_todo") }}</a></li>
|
||||
|
||||
|
||||
@dispatchEvent("submenuSection", ["ticket"=>$ticket])
|
||||
|
||||
<li class="nav-header border">{{ __("subtitles.track_time") }}</li>
|
||||
@dispatchEvent("beforeTimer", ["ticket"=>$ticket])
|
||||
<li class="timerContainer tw-px-[10px]">
|
||||
@include('tickets::partials.timerButton', ['parentTicketId' => $ticket['id'], 'onTheClock' => $onTheClock, 'style'=> 'full'])
|
||||
</li>
|
||||
@dispatchEvent("end")
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
78
app/Domain/Tickets/Templates/partials/timerButton.blade.php
Normal file
78
app/Domain/Tickets/Templates/partials/timerButton.blade.php
Normal file
@@ -0,0 +1,78 @@
|
||||
@props([
|
||||
'parentTicketId' => false,
|
||||
'onTheClock' => false,
|
||||
'style' => 'simple' //simple just the button, full witrh button text
|
||||
])
|
||||
|
||||
<div id="timer-button-container-{{ $parentTicketId }}"
|
||||
hx-get="{{BASE_URL}}/tickets/timerButton/get-status-button/{{ $parentTicketId }}"
|
||||
hx-trigger="timerUpdate from:body"
|
||||
hx-swap="outerHTML"
|
||||
|
||||
class="tw-relative timerContainer">
|
||||
|
||||
@if ($onTheClock === false)
|
||||
<a href="javascript:void(0);" data-value="{{ $parentTicketId }}"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/start-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-swap="outerHTML"
|
||||
onclick="this.classList.add('starting');"
|
||||
hx-vals='{"ticketId": "{{ $parentTicketId }}", "action":"start"}'
|
||||
data-tippy-content="{{ __("links.start_work") }}">
|
||||
|
||||
<span class="fa-regular fa-circle-play" style="font-size:18px; padding-top:3px;"></span>
|
||||
|
||||
@if($style=="full")
|
||||
{{ __("links.start_work") }}
|
||||
@endif
|
||||
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($onTheClock !== false && $onTheClock["id"] == $parentTicketId)
|
||||
<a href="javascript:void(0);" data-value="{{ $parentTicketId }}"
|
||||
hx-trigger="click delay:500ms"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/stop-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-vals='{"ticketId": "{{ $parentTicketId }}", "action":"stop"}'
|
||||
hx-swap="outerHTML"
|
||||
onclick="this.classList.add('stopped');"
|
||||
data-tippy-content="@if (is_array($onTheClock) == true) {!! strip_tags(sprintf(__("links.stop_work_started_at"), dtHelper()::createFromTimestamp($onTheClock["since"], 'UTC')->setToUserTimezone()->format(__("language.timeformat")))) !!} @else {!! strip_tags(sprintf(__("links.stop_work_started_at"), dtHelper()::now()->setToUserTimezone()->format(__("language.timeformat")))) !!} @endif"
|
||||
>
|
||||
|
||||
|
||||
<span class="fa-regular fa-circle-stop" style="font-size:18px; padding-top:3px;"></span>
|
||||
@if($style=="full")
|
||||
@if (is_array($onTheClock) == true)
|
||||
{!! sprintf(__("links.stop_work_started_at"), dtHelper()::createFromTimestamp($onTheClock["since"], 'UTC')->setToUserTimezone()->format(__("language.timeformat"))) !!}
|
||||
@else
|
||||
{!! sprintf(__("links.stop_work_started_at"), dtHelper()::now()->setToUserTimezone()->format(__("language.timeformat"))) !!}
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<!-- These elements will be added dynamically when the timer is stopped -->
|
||||
<div class="success-circle"></div>
|
||||
<div class="particles-container">
|
||||
<div class="particle particle-1"></div>
|
||||
<div class="particle particle-2"></div>
|
||||
<div class="particle particle-3"></div>
|
||||
<div class="particle particle-4"></div>
|
||||
<div class="particle particle-5"></div>
|
||||
<div class="particle particle-6"></div>
|
||||
<div class="particle particle-7"></div>
|
||||
<div class="particle particle-8"></div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
@endif
|
||||
@if ($onTheClock !== false && $onTheClock["id"] != $parentTicketId)
|
||||
<span class='working'>
|
||||
@if($style=="full")
|
||||
{{ __("text.timer_set_other_todo") }}
|
||||
@else
|
||||
<span class="fa-solid fa-user-clock" style="font-size:16px; padding-top:3px; color:var(--grey);" data-tippy-content="{{ __("text.timer_set_other_todo") }}"></span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
43
app/Domain/Tickets/Templates/partials/timerLink.blade.php
Normal file
43
app/Domain/Tickets/Templates/partials/timerLink.blade.php
Normal file
@@ -0,0 +1,43 @@
|
||||
@props([
|
||||
'parentTicketId' => false,
|
||||
'onTheClock' => false
|
||||
])
|
||||
|
||||
<li id="timerContainer-{{ $parentTicketId }}"
|
||||
hx-get="{{BASE_URL}}/tickets/timerButton/get-status/{{ $parentTicketId }}"
|
||||
hx-trigger="timerUpdate from:body"
|
||||
hx-swap="outerHTML"
|
||||
class="timerContainer">
|
||||
|
||||
@if ($onTheClock === false)
|
||||
<a href="javascript:void(0);" data-value="{{ $parentTicketId }}"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/start-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"ticketId": "{{ $parentTicketId }}", "action":"start"}'>
|
||||
<span class="fa-regular fa-clock"></span> {{ __("links.start_work") }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($onTheClock !== false && $onTheClock["id"] == $parentTicketId)
|
||||
<a href="javascript:void(0);" data-value="{{ $parentTicketId }}"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/stop-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-vals='{"ticketId": "{{ $parentTicketId }}", "action":"stop"}'
|
||||
hx-swap="outerHTML">
|
||||
<span class="fa fa-stop"></span>
|
||||
|
||||
@if (is_array($onTheClock) == true)
|
||||
{!! sprintf(__("links.stop_work_started_at"), date(__("language.timeformat"), $onTheClock["since"])) !!}
|
||||
@else
|
||||
{!! sprintf(__("links.stop_work_started_at"), date(__("language.timeformat"), time())) !!}
|
||||
@endif
|
||||
</a>
|
||||
@endif
|
||||
@if ($onTheClock !== false && $onTheClock["id"] != $parentTicketId)
|
||||
<span class='working'>
|
||||
{{ __("text.timer_set_other_todo") }}
|
||||
</span>
|
||||
@endif
|
||||
</li>
|
||||
|
||||
165
app/Domain/Tickets/Templates/roadmap.blade.php
Normal file
165
app/Domain/Tickets/Templates/roadmap.blade.php
Normal file
@@ -0,0 +1,165 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$milestones = $milestones ?? [];
|
||||
$timelineTasks = $timelineTasks ?? [];
|
||||
$roadmapView = session('usersettings.views.roadmap', 'Month');
|
||||
@endphp
|
||||
|
||||
@include('tickets::submodules.timelineHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
@include('tickets::submodules.timelineTabs')
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@dispatchEvent('filters.afterLefthandSectionOpen')
|
||||
|
||||
@include('tickets::submodules.ticketNewBtn')
|
||||
@include('tickets::submodules.ticketFilter')
|
||||
|
||||
@dispatchEvent('filters.beforeLefthandSectionClose')
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="pull-right">
|
||||
|
||||
<div class="btn-group dropRight">
|
||||
|
||||
@php
|
||||
$currentView = '';
|
||||
if ($roadmapView == 'Day') {
|
||||
$currentView = __('buttons.day');
|
||||
} elseif ($roadmapView == 'Week') {
|
||||
$currentView = __('buttons.week');
|
||||
} elseif ($roadmapView == 'Month') {
|
||||
$currentView = __('buttons.month');
|
||||
}
|
||||
@endphp
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">{!! __('buttons.timeframe') !!}: <span class="viewText">{{ $currentView }}</span><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" id="ganttTimeControl">
|
||||
<li><a href="javascript:void(0);" data-value="Day" class="{{ $roadmapView == 'Day' ? 'active' : '' }}"> {!! __('buttons.day') !!}</a></li>
|
||||
<li><a href="javascript:void(0);" data-value="Week" class="{{ $roadmapView == 'Week' ? 'active' : '' }}">{!! __('buttons.week') !!}</a></li>
|
||||
<li><a href="javascript:void(0);" data-value="Month" class="{{ $roadmapView == 'Month' ? 'active' : '' }}">{!! __('buttons.month') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
if (
|
||||
(is_array($timelineTasks) && count($timelineTasks) == 0) ||
|
||||
$timelineTasks == false
|
||||
) {
|
||||
echo "<div class='empty' id='emptySprint' style='text-align:center;'>";
|
||||
echo "<div style='width:30%' class='svgContainer'>";
|
||||
echo file_get_contents(ROOT.'/dist/images/svg/undraw_adjustments_p22m.svg');
|
||||
echo '</div>';
|
||||
echo '<h4>'.__('headlines.no_tickets').'<br /></h4></div>';
|
||||
}
|
||||
@endphp
|
||||
<div class="gantt-wrapper">
|
||||
<svg id="gantt"></svg>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
|
||||
@if (isset($_GET['showMilestoneModal']))
|
||||
@php
|
||||
$modalUrl = $_GET['showMilestoneModal'] == '' ? '' : '/'.(int) $_GET['showMilestoneModal'];
|
||||
@endphp
|
||||
|
||||
leantime.ticketsController.openMilestoneModalManually("{{ BASE_URL }}/tickets/editMilestone{{ $modalUrl }}");
|
||||
window.history.pushState({},document.title, '{{ BASE_URL }}/tickets/roadmap');
|
||||
|
||||
@endif
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
@php
|
||||
|
||||
if (count($timelineTasks) > 0) {
|
||||
@endphp
|
||||
var tasks = [
|
||||
|
||||
@php
|
||||
$lastMilestoneSortIndex = [];
|
||||
|
||||
foreach ($timelineTasks as $mlst) {
|
||||
if ($mlst->type == 'milestone') {
|
||||
$lastMilestoneSortIndex[$mlst->id] = ($mlst->sortIndex != '') ? $mlst->sortIndex : 999;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($timelineTasks as $mlst) {
|
||||
|
||||
$headline = __('label.'.strtolower($mlst->type)).': '.$mlst->headline;
|
||||
if ($mlst->type == 'milestone') {
|
||||
$headline .= ' ('.format($mlst->percentDone)->decimal().'% Done)';
|
||||
}
|
||||
|
||||
$color = '#8D99A6';
|
||||
if ($mlst->type == 'milestone') {
|
||||
$color = $mlst->tags;
|
||||
}
|
||||
|
||||
$sortIndex = $mlst->sortIndex;
|
||||
|
||||
$dependencyList = [];
|
||||
|
||||
// Use explicit > 0 checks: new milestones store dependingTicketId as an empty
|
||||
// string, and under PHP 8 `'' != 0` is true — which pushed an empty dependency and
|
||||
// skipped the real milestoneid, so a dependency set in table mode never rendered here.
|
||||
if ((int) $mlst->dependingTicketId > 0) {
|
||||
$dependencyList[] = $mlst->dependingTicketId;
|
||||
} elseif ((int) $mlst->milestoneid > 0) {
|
||||
$dependencyList[] = $mlst->milestoneid;
|
||||
}
|
||||
|
||||
echo "{
|
||||
id :'".$mlst->id."',
|
||||
name :".json_encode($headline, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP).",
|
||||
start :'".(dtHelper()->isValidDateString($mlst->editFrom) ? $mlst->editFrom : dtHelper()->userNow()->addDays(2)->format('Y-m-d'))."',
|
||||
end :'".(dtHelper()->isValidDateString($mlst->editTo) ? $mlst->editTo : dtHelper()->userNow()->addDays(2)->format('Y-m-d'))."',
|
||||
progress :'".format($mlst->percentDone)->decimal()."',
|
||||
dependencies :'".implode(',', $dependencyList)."',
|
||||
custom_class :'',
|
||||
type: '".strtolower($mlst->type)."',
|
||||
bg_color: ".json_encode($color, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP).",
|
||||
thumbnail: '".BASE_URL.'/api/users?profileImage='.$mlst->editorId."',
|
||||
sortIndex: ".$sortIndex.'
|
||||
},';
|
||||
}
|
||||
@endphp
|
||||
];
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ticketsController.initGanttChart(tasks, '{{ $roadmapView }}', false);
|
||||
@else
|
||||
leantime.ticketsController.initGanttChart(tasks, '{{ $roadmapView }}', true);
|
||||
@endif
|
||||
|
||||
@php } @endphp
|
||||
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
171
app/Domain/Tickets/Templates/roadmapAll.blade.php
Normal file
171
app/Domain/Tickets/Templates/roadmapAll.blade.php
Normal file
@@ -0,0 +1,171 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$milestones = $milestones ?? [];
|
||||
$clients = $clients ?? [];
|
||||
|
||||
$clientNameSelected = __('headline.all_clients');
|
||||
$htmlDropdownClients = '';
|
||||
foreach ($clients as $client) {
|
||||
$href = BASE_URL.'/tickets/roadmapAll?clientId='.$client['id'];
|
||||
$labelActive = '';
|
||||
if (isset($_GET['clientId']) && $_GET['clientId'] == $client['id']) {
|
||||
$labelActive = ' class="active"';
|
||||
$clientNameSelected = $client['name'];
|
||||
}
|
||||
$htmlDropdownClients .= "<li><a href='$href' $labelActive> {$client['name']} </a></li>";
|
||||
}
|
||||
|
||||
$roadmapView = session('usersettings.views.roadmap', 'Month');
|
||||
@endphp
|
||||
|
||||
@include('tickets::submodules.portfolioHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
@include('tickets::submodules.portfolioTabs')
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="pull-right">
|
||||
|
||||
<div class="btn-group viewDropDown">
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">{!! __('label.roles.client') !!}: <span class="viewText">{{ $clientNameSelected }}</span><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href={{ BASE_URL.'/tickets/roadmapAll' }} {{ empty($labelActive) ? "class='active'" : '' }} > {!! __('headline.all_clients') !!} </a></li>
|
||||
{!! $htmlDropdownClients !!}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group dropRight">
|
||||
|
||||
@php
|
||||
$currentView = '';
|
||||
if ($roadmapView == 'Day') {
|
||||
$currentView = __('buttons.day');
|
||||
} elseif ($roadmapView == 'Week') {
|
||||
$currentView = __('buttons.week');
|
||||
} elseif ($roadmapView == 'Month') {
|
||||
$currentView = __('buttons.month');
|
||||
}
|
||||
@endphp
|
||||
<button class="btn dropdown-toggle" data-toggle="dropdown">{!! __('buttons.timeframe') !!}: <span class="viewText">{{ $currentView }}</span><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu" id="ganttTimeControl">
|
||||
<li><a href="javascript:void(0);" data-value="Day" class="{{ $roadmapView == 'Day' ? 'active' : '' }}"> {!! __('buttons.day') !!}</a></li>
|
||||
<li><a href="javascript:void(0);" data-value="Week" class="{{ $roadmapView == 'Week' ? 'active' : '' }}">{!! __('buttons.week') !!}</a></li>
|
||||
<li><a href="javascript:void(0);" data-value="Month" class="{{ $roadmapView == 'Month' ? 'active' : '' }}">{!! __('buttons.month') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
if (count($milestones) == 0) {
|
||||
echo "<div class='empty' id='emptySprint' style='text-align:center;'>";
|
||||
echo "<div style='width:30%' class='svgContainer'>";
|
||||
echo file_get_contents(ROOT.'/dist/images/svg/undraw_adjustments_p22m.svg');
|
||||
echo '</div>';
|
||||
echo '<h4>'.__('headlines.no_milestones').'<br/>
|
||||
<br />
|
||||
<a href="'.BASE_URL.'/tickets/editMilestone" class="milestoneModal addCanvasLink btn btn-primary">'.__('links.add_milestone').'</a></h4></div>';
|
||||
}
|
||||
@endphp
|
||||
<div class="gantt-wrapper">
|
||||
<svg id="gantt"></svg>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
|
||||
@if (isset($_GET['showMilestoneModal']))
|
||||
@php
|
||||
$modalUrl = $_GET['showMilestoneModal'] == '' ? '' : '/'.(int) $_GET['showMilestoneModal'];
|
||||
@endphp
|
||||
|
||||
leantime.ticketsController.openMilestoneModalManually("{{ BASE_URL }}/tickets/editMilestone{{ $modalUrl }}");
|
||||
window.history.pushState({},document.title, '{{ BASE_URL }}/tickets/roadmap');
|
||||
|
||||
@endif
|
||||
|
||||
|
||||
});
|
||||
|
||||
@if (count($milestones) > 0)
|
||||
var tasks = [
|
||||
|
||||
@php
|
||||
foreach ($milestones as $mlst) {
|
||||
$headline = '['.$mlst->projectName.'] ';
|
||||
$headline .= __('label.'.strtolower($mlst->type)).': '.$mlst->headline;
|
||||
if ($mlst->type == 'milestone') {
|
||||
$headline .= ' ('.$mlst->percentDone.'% Done)';
|
||||
}
|
||||
|
||||
$color = '#8D99A6';
|
||||
if ($mlst->type == 'milestone') {
|
||||
$color = $mlst->tags;
|
||||
}
|
||||
|
||||
$sortIndex = 0;
|
||||
if ($mlst->sortIndex != '' && is_numeric($mlst->sortIndex)) {
|
||||
$sortIndex = $mlst->sortIndex;
|
||||
}
|
||||
|
||||
$dependencyList = [];
|
||||
if ($mlst->milestoneid != 0) {
|
||||
$dependencyList[] = $mlst->milestoneid;
|
||||
}
|
||||
|
||||
if ($mlst->dependingTicketId != 0) {
|
||||
$dependencyList[] = $mlst->dependingTicketId;
|
||||
}
|
||||
|
||||
echo "{
|
||||
projectName :'".$mlst->projectName."',
|
||||
id :'".$mlst->id."',
|
||||
name :".json_encode($headline).",
|
||||
start :'".(($mlst->editFrom != '0000-00-00 00:00:00' && ! str_starts_with($mlst->editFrom, '1969-12-31')) ? $mlst->editFrom : date('Y-m-d', strtotime('+1 day', time())))."',
|
||||
end :'".(($mlst->editTo != '0000-00-00 00:00:00' && ! str_starts_with($mlst->editTo, '1969-12-31')) ? $mlst->editTo : date('Y-m-d', strtotime('+1 week', time())))."',
|
||||
progress :'".$mlst->percentDone."',
|
||||
dependencies :'".implode(',', $dependencyList)."',
|
||||
custom_class :'',
|
||||
type: '".strtolower($mlst->type)."',
|
||||
bg_color: '".$color."',
|
||||
thumbnail: '".BASE_URL.'/api/users?profileImage='.$mlst->editorId."',
|
||||
sortIndex: ".$sortIndex.'
|
||||
|
||||
},';
|
||||
}
|
||||
@endphp
|
||||
];
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ticketsController.initGanttChart(tasks, '{{ $roadmapView }}', false);
|
||||
@else
|
||||
leantime.ticketsController.initGanttChart(tasks, '{{ $roadmapView }}', true);
|
||||
@endif
|
||||
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
393
app/Domain/Tickets/Templates/showAll.blade.php
Normal file
393
app/Domain/Tickets/Templates/showAll.blade.php
Normal file
@@ -0,0 +1,393 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$allTicketGroups = $allTickets;
|
||||
$todoTypeIcons = $ticketTypeIcons;
|
||||
$statusLabels = $allTicketStates;
|
||||
$newField = $newField ?? [];
|
||||
$numberofColumns = count($allTicketStates) - 1;
|
||||
$size = floor(100 / $numberofColumns);
|
||||
@endphp
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@include('tickets::submodules.ticketHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
@include('tickets::submodules.ticketBoardTabs')
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
{{-- Board actions (New / Filter / Group By) moved into the nav bar
|
||||
(ticketBoardTabs). Only the table-specific DataTables buttons remain
|
||||
here, right-aligned above the table. --}}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="pull-right">
|
||||
@dispatchEvent('filters.afterRighthandSectionOpen')
|
||||
<div id="tableButtons" style="display:inline-block"></div>
|
||||
@dispatchEvent('filters.beforeRighthandSectionClose')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="margin-bottom: 20px;"></div>
|
||||
|
||||
@if (isset($availableProjects))
|
||||
{{-- Program (cross-project) board: consistent inline quick-add with a required
|
||||
project picker, matching the kanban/list add affordance. --}}
|
||||
<form action="" method="post" class="tw-mb-m" style="display:flex; gap:10px; align-items:flex-start; flex-wrap:wrap;">
|
||||
<input type="text" name="headline" placeholder="{{ __('input.placeholders.create_task') }}" style="flex:1 1 280px; min-width:240px;" />
|
||||
<select name="quickaddProjectId" class="form-control" required style="width:auto;" aria-label="{{ __('label.project') }}">
|
||||
<option value="">{{ __('label.project') }}…</option>
|
||||
@foreach ($availableProjects as $quickAddProjectId => $quickAddProjectName)
|
||||
<option value="{{ $quickAddProjectId }}">{{ $tpl->escape($quickAddProjectName) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="hidden" name="sprint" value="{{ $currentSprint }}" />
|
||||
<input type="hidden" name="milestone" value="{{ htmlspecialchars((string) ($searchCriteria['milestone'] ?? ''), ENT_QUOTES, 'UTF-8') }}" />
|
||||
<input type="hidden" name="quickadd" value="1" />
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="saveTicket" />
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if (isset($allTicketGroups['all']))
|
||||
@php $allTickets = $allTicketGroups['all']['items']; @endphp
|
||||
@endif
|
||||
|
||||
@foreach ($allTicketGroups as $group)
|
||||
@if ($group['label'] != 'all')
|
||||
<h5 class="accordionTitle {{ $group['class'] }}" @if (!empty($group['color'])) style="color:{{ htmlspecialchars($group['color']) }}" @endif id="accordion_link_{{ $group['id'] }}">
|
||||
<a href="javascript:void(0)" class="accordion-toggle" id="accordion_toggle_{{ $group['id'] }}" onclick="leantime.snippets.accordionToggle('{{ $group['id'] }}');">
|
||||
<i class="fa fa-angle-down"></i>{{ $group['label'] }}({{ count($group['items']) }})
|
||||
</a><br />
|
||||
<small style="padding-left:20px; color:var(--primary-font-color); font-size:var(--font-size-s);">{{ $group['more-info'] }}</small>
|
||||
</h5>
|
||||
|
||||
<div class="simpleAccordionContainer" id="accordion_content-{{ $group['id'] }}">
|
||||
@endif
|
||||
|
||||
@php $allTickets = $group['items']; @endphp
|
||||
|
||||
@dispatchEvent('allTicketsTable.before', ['tickets' => $allTicketGroups])
|
||||
<table class="table table-bordered display ticketTable " style="width:100%">
|
||||
<colgroup>
|
||||
<col class="con1">
|
||||
<col class="con0" style="max-width:200px;">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
</colgroup>
|
||||
@dispatchEvent('allTicketsTable.beforeHead', ['tickets' => $allTickets])
|
||||
<thead>
|
||||
@dispatchEvent('allTicketsTable.beforeHeadRow', ['tickets' => $allTickets])
|
||||
<tr>
|
||||
<th class="id-col">{!! __('label.id') !!}</th>
|
||||
<th style="max-width: 350px;">{!! __('label.title') !!}</th>
|
||||
<th class="status-col">{!! __('label.todo_status') !!}</th>
|
||||
<th class="milestone-col">{!! __('label.milestone') !!}</th>
|
||||
<th class="effort-col">{!! __('label.effort') !!}</th>
|
||||
<th class="priority-col">{!! __('label.priority') !!}</th>
|
||||
<th class="user-col">{!! __('label.editor') !!}.</th>
|
||||
<th class="sprint-col">{!! __('label.sprint') !!}</th>
|
||||
<th class="tags-col">{!! __('label.tags') !!}</th>
|
||||
<th class="duedate-col">{!! __('label.due_date') !!}</th>
|
||||
<th class="planned-hours-col">{!! __('label.planned_hours') !!}</th>
|
||||
<th class="remaining-hours-col">{!! __('label.estimated_hours_remaining') !!}</th>
|
||||
<th class="booked-hours-col">{!! __('label.booked_hours') !!}</th>
|
||||
<th class="no-sort"></th>
|
||||
</tr>
|
||||
@dispatchEvent('allTicketsTable.afterHeadRow', ['tickets' => $allTickets])
|
||||
</thead>
|
||||
@dispatchEvent('allTicketsTable.afterHead', ['tickets' => $allTickets])
|
||||
<tbody>
|
||||
@dispatchEvent('allTicketsTable.beforeFirstRow', ['tickets' => $allTickets])
|
||||
@foreach ($allTickets as $rowNum => $row)
|
||||
<tr style="height:1px;">
|
||||
@dispatchEvent('allTicketsTable.afterRowStart', ['rowNum' => $rowNum, 'tickets' => $allTickets])
|
||||
<td data-order="{{ $row['id'] }}">
|
||||
#{{ $row['id'] }}
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row['headline'] }}">
|
||||
@if ($row['dependingTicketId'] > 0)
|
||||
<small><a href="#/tickets/showTicket/{{ $row['dependingTicketId'] }}" preload="mouseover">{{ $row['parentHeadline'] }}</a></small> //<br />
|
||||
@endif
|
||||
<a class='ticketModal' href="#/tickets/showTicket/{{ $row['id'] }}" preload="mouseover">{{ $row['headline'] }}</a></td>
|
||||
|
||||
@php
|
||||
// On a program (cross-project) board each row must render and edit
|
||||
// with statuses from ITS OWN project, never a shared set, so a status
|
||||
// change always writes a key valid in that project.
|
||||
$rowStatusLabels = (isset($statusLabelsByProject) && isset($statusLabelsByProject[$row['projectId']]))
|
||||
? $statusLabelsByProject[$row['projectId']]
|
||||
: $statusLabels;
|
||||
|
||||
if (isset($rowStatusLabels[$row['status']])) {
|
||||
$class = $rowStatusLabels[$row['status']]['class'];
|
||||
$name = $rowStatusLabels[$row['status']]['name'];
|
||||
$sortKey = $rowStatusLabels[$row['status']]['sortKey'];
|
||||
} else {
|
||||
$class = 'label-important';
|
||||
$name = 'new';
|
||||
$sortKey = 0;
|
||||
}
|
||||
@endphp
|
||||
<td data-order="{{ $name }}">
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show ">
|
||||
<a class="dropdown-toggle status {{ $class }} f-left" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['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{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_status') !!}</li>
|
||||
@php
|
||||
foreach ($rowStatusLabels as $key => $label) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='".$label['class']."' data-label='".$tpl->escape($label['name'])."' data-value='".$row['id'].'_'.$key.'_'.$label['class']."' id='ticketStatusChange".$row['id'].$key."' >".$tpl->escape($label['name']).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@php
|
||||
if ($row['milestoneid'] != '' && $row['milestoneid'] != 0) {
|
||||
$milestoneHeadline = $tpl->escape($row['milestoneHeadline']);
|
||||
} else {
|
||||
$milestoneHeadline = __('label.no_milestone');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<td data-order="{{ $milestoneHeadline }}">
|
||||
<div class="dropdown ticketDropdown milestoneDropdown colorized show">
|
||||
<a style="background-color:{{ $tpl->escape($row['milestoneColor']) }}" class="dropdown-toggle label-default milestone f-left" href="javascript:void(0);" role="button" id="milestoneDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $milestoneHeadline }}</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="milestoneDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_milestone') !!}</li>
|
||||
<li class='dropdown-item'><a style='background-color:#b0b0b0' href='javascript:void(0);' data-label="{!! __('label.no_milestone') !!}" data-value='{{ $row['id'].'_0_#b0b0b0' }}'> {!! __('label.no_milestone') !!} </a></li>
|
||||
|
||||
@php
|
||||
foreach ($milestones as $milestone) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".$tpl->escape($milestone->headline)."' data-value='".$row['id'].'_'.$milestone->id.'_'.$tpl->escape($milestone->tags)."' id='ticketMilestoneChange".$row['id'].$milestone->id."' style='background-color:".$tpl->escape($milestone->tags)."'>".$tpl->escape($milestone->headline).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td data-order="{{ $row['storypoints'] ? $efforts[''.$row['storypoints'].''] ?? '?' : __('label.story_points_unkown') }}">
|
||||
<div class="dropdown ticketDropdown effortDropdown show">
|
||||
<a class="dropdown-toggle label-default effort f-left" href="javascript:void(0);" role="button" id="effortDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">@if ($row['storypoints'] != '' && $row['storypoints'] > 0){{ $efforts[''.$row['storypoints']] ?? $row['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{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.how_big_todo') !!}</li>
|
||||
@php
|
||||
foreach ($efforts as $effortKey => $effortValue) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-value='".$row['id'].'_'.$effortKey."' id='ticketEffortChange".$row['id'].$effortKey."'>".$effortValue.'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-order="@php if ($row['priority'] != '' && $row['priority'] > 0) { echo $priorities[$row['priority']] ?? __('label.priority_unkown'); } else { echo __('label.priority_unkown'); } @endphp">
|
||||
<div class="dropdown ticketDropdown priorityDropdown show">
|
||||
<a class="dropdown-toggle label-default priority priority-bg-{{ $row['priority'] }} f-left" href="javascript:void(0);" role="button" id="priorityDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">@php if ($row['priority'] != '' && $row['priority'] > 0) { echo $priorities[$row['priority']] ?? __('label.priority_unkown'); } else { echo __('label.priority_unkown'); } @endphp</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="priorityDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.select_priority') !!}</li>
|
||||
@php
|
||||
foreach ($priorities as $priorityKey => $priorityValue) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='priority-bg-".$priorityKey."' data-value='".$row['id'].'_'.$priorityKey."' id='ticketPriorityChange".$row['id'].$priorityKey."'>".$priorityValue.'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td data-order="{{ $row['editorFirstname'] != '' ? $tpl->escape($row['editorFirstname']) : __('dropdown.not_assigned') }}">
|
||||
<div class="dropdown ticketDropdown userDropdown noBg show f-left">
|
||||
<a class="dropdown-toggle" href="javascript:void(0);" role="button" id="userDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text" style="display:inline-flex; align-items:center; gap:6px;">
|
||||
@php
|
||||
if ($row['editorFirstname'] != '') {
|
||||
echo "<span id='userImage".$row['id']."'><img src='".BASE_URL.'/api/users?profileImage='.$row['editorId']."' width='25' style='vertical-align: middle; margin-right:5px;'/></span><span id='user".$row['id']."'>".$tpl->escape($row['editorFirstname']).'</span>';
|
||||
} else {
|
||||
echo "<span id='userImage".$row['id']."'><img src='".BASE_URL."/api/users?profileImage=false' width='25' style='vertical-align: middle; margin-right:5px;'/></span><span id='user".$row['id']."'>".__('dropdown.not_assigned').'</span>';
|
||||
}
|
||||
|
||||
if (! empty($row['collaboratorPreview'])) {
|
||||
echo "<span class='ticket-collaborators' style='display:inline-flex; align-items:center; margin-left:4px;'>";
|
||||
foreach ($row['collaboratorPreview'] as $index => $collaboratorId) {
|
||||
$offset = $index > 0 ? 'margin-left:-8px;' : '';
|
||||
echo "<span class='ticket-collaborator-avatar' title='".__('label.collaborators')."' style='display:inline-flex; width:20px; height:20px; border-radius:999px; border:2px solid var(--main-background-color, #fff); overflow:hidden; ".$offset."'><img src='".BASE_URL.'/api/users?profileImage='.$collaboratorId."' width='20' height='20' style='display:block; width:20px; height:20px;'/></span>";
|
||||
}
|
||||
if (($row['collaboratorOverflow'] ?? 0) > 0) {
|
||||
echo "<span class='ticket-collaborator-more' title='".__('label.collaborators')."' style='display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px; padding:0 5px; margin-left:4px; border-radius:999px; background:var(--accent-color, #e9ecef); color:var(--secondary-font-color, #333); font-size:11px; line-height:20px;'>+".(int) $row['collaboratorOverflow'].'</span>';
|
||||
}
|
||||
echo '</span>';
|
||||
}
|
||||
@endphp
|
||||
</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_user') !!}</li>
|
||||
<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='{!! __('label.not_assigned_to_user') !!}' data-value='{{ $row['id'].'_0_0' }}' id='userStatusChange{{ $row['id'] }}0' >{!! __('label.not_assigned_to_user') !!}</a>
|
||||
</li>
|
||||
@php
|
||||
foreach ($users as $user) {
|
||||
echo "<li class='dropdown-item'>";
|
||||
echo "<a href='javascript:void(0);' data-label='".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname']))."' data-value='".$row['id'].'_'.$user['id'].'_'.$user['profileId']."' id='userStatusChange".$row['id'].$user['id']."' ><img src='".BASE_URL.'/api/users?profileImage='.$user['id']."' width='25' style='vertical-align: middle; margin-right:5px;'/>".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
@php
|
||||
if ($row['sprint'] != '' && $row['sprint'] != 0 && $row['sprint'] != -1) {
|
||||
$sprintHeadline = $tpl->escape($row['sprintName']);
|
||||
} else {
|
||||
$sprintHeadline = __('label.not_assigned_to_sprint');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<td data-order="{{ $sprintHeadline }}">
|
||||
|
||||
<div class="dropdown ticketDropdown sprintDropdown show">
|
||||
<a class="dropdown-toggle label-default sprint f-left" href="javascript:void(0);" role="button" id="sprintDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $sprintHeadline }}</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="sprintDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_sprint') !!}</li>
|
||||
<li class='dropdown-item'><a href='javascript:void(0);' data-label="{!! __('label.not_assigned_to_sprint') !!}" data-value='{{ $row['id'].'_0' }}'> {!! __('label.not_assigned_to_sprint') !!} </a></li>
|
||||
@if ($sprints)
|
||||
@foreach ($sprints as $sprint)
|
||||
<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='{{ $sprint->name }}' data-value='{{ $row['id'].'_'.$sprint->id }}' id='ticketSprintChange{{ $row['id'] }}{{ $sprint->id }}' >{{ $sprint->name }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row['tags'] }}">
|
||||
@if ($row['tags'] != '')
|
||||
@php $tagsArray = explode(',', $row['tags']); @endphp
|
||||
<div class='tagsinput readonly'>
|
||||
@foreach ($tagsArray as $tag)
|
||||
<span class='tag'><span>{{ $tag }}</span></span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@php
|
||||
if ($row['dateToFinish'] == '0000-00-00 00:00:00' || $row['dateToFinish'] == '1969-12-31 00:00:00') {
|
||||
$date = __('text.anytime');
|
||||
} else {
|
||||
$date = new DateTime($row['dateToFinish']);
|
||||
$date = $date->format(__('language.dateformat'));
|
||||
}
|
||||
@endphp
|
||||
<td data-order="{{ $row['dateToFinish'] }}" >
|
||||
<input type="text" title="{{ __('label.due') }}" value="{{ $date }}" class="quickDueDates secretInput" data-id="{{ $row['id'] }}" name="date" />
|
||||
</td>
|
||||
<td data-order="{{ $row['planHours'] }}">
|
||||
<input type="text" value="{{ $row['planHours'] }}" name="planHours" class="small-input secretInput" onchange="leantime.ticketsController.updatePlannedHours(this, '{{ $row['id'] }}'); jQuery(this).parent().attr('data-order',jQuery(this).val());" />
|
||||
</td>
|
||||
<td data-order="{{ $row['hourRemaining'] }}">
|
||||
<input type="text" value="{{ $row['hourRemaining'] }}" name="remainingHours" class="small-input secretInput" onchange="leantime.ticketsController.updateRemainingHours(this, '{{ $row['id'] }}');" />
|
||||
</td>
|
||||
|
||||
<td data-order="{{ ($row['bookedHours'] === null || $row['bookedHours'] == '') ? '0' : $row['bookedHours'] }}">
|
||||
{{ ($row['bookedHours'] === null || $row['bookedHours'] == '') ? '0' : $row['bookedHours'] }}
|
||||
</td>
|
||||
<td>
|
||||
@include('tickets::partials.ticketsubmenu', ['ticket' => $row, 'onTheClock' => $onTheClock])
|
||||
</td>
|
||||
@dispatchEvent('allTicketsTable.beforeRowEnd', ['tickets' => $allTickets, 'rowNum' => $rowNum])
|
||||
</tr>
|
||||
@endforeach
|
||||
@dispatchEvent('allTicketsTable.afterLastRow', ['tickets' => $allTickets])
|
||||
</tbody>
|
||||
@dispatchEvent('allTicketsTable.afterBody', ['tickets' => $allTickets])
|
||||
<tfoot align="right">
|
||||
<tr><td colspan="9"></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
</tfoot>
|
||||
|
||||
</table>
|
||||
@dispatchEvent('allTicketsTable.afterClose', ['tickets' => $allTickets])
|
||||
|
||||
@if ($group['label'] != 'all')
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
leantime.ticketsController.initUserDropdown();
|
||||
leantime.ticketsController.initMilestoneDropdown();
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initPriorityDropdown();
|
||||
leantime.ticketsController.initSprintDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
leantime.ticketsController.initTicketsTable("{{ $searchCriteria['groupBy'] }}");
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
286
app/Domain/Tickets/Templates/showAllMilestones.blade.php
Normal file
286
app/Domain/Tickets/Templates/showAllMilestones.blade.php
Normal file
@@ -0,0 +1,286 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$allTicketGroups = $allTickets;
|
||||
$todoTypeIcons = $ticketTypeIcons;
|
||||
$statusLabels = $allTicketStates;
|
||||
$numberofColumns = count($allTicketStates) - 1;
|
||||
$size = floor(100 / $numberofColumns);
|
||||
@endphp
|
||||
|
||||
@include('tickets::submodules.timelineHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
@include('tickets::submodules.timelineTabs')
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@dispatchEvent('filters.afterLefthandSectionOpen')
|
||||
@include('tickets::submodules.ticketNewBtn')
|
||||
@include('tickets::submodules.ticketFilter')
|
||||
@dispatchEvent('filters.beforeLefthandSectionClose')
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="pull-right">
|
||||
@dispatchEvent('filters.afterRighthandSectionOpen')
|
||||
<div id="tableButtons" style="display:inline-block"></div>
|
||||
@dispatchEvent('filters.beforeRighthandSectionClose')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="margin-bottom: 20px;"></div>
|
||||
|
||||
@if (isset($allTicketGroups['all']))
|
||||
@php $allTickets = $allTicketGroups['all']['items']; @endphp
|
||||
@endif
|
||||
|
||||
@foreach ($allTicketGroups as $group)
|
||||
@if ($group['label'] != 'all')
|
||||
<h5 class="accordionTitle {{ $group['class'] }}" @if (!empty($group['color'])) style="color:{{ htmlspecialchars($group['color']) }}" @endif id="accordion_link_{{ $group['id'] }}">
|
||||
<a href="javascript:void(0)" class="accordion-toggle" id="accordion_toggle_{{ $group['id'] }}" onclick="leantime.snippets.accordionToggle('{{ $group['id'] }}');">
|
||||
<i class="fa fa-angle-down"></i>{{ $group['label'] }} ({{ count($group['items']) }})
|
||||
</a>
|
||||
</h5>
|
||||
<div class="simpleAccordionContainer" id="accordion_content-{{ $group['id'] }}">
|
||||
@endif
|
||||
|
||||
@php $allTickets = $group['items']; @endphp
|
||||
|
||||
@dispatchEvent('allTicketsTable.before', ['tickets' => $allTickets])
|
||||
<table class="table table-bordered display ticketTable " style="width:100%">
|
||||
<colgroup>
|
||||
<col class="con1" >
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0" >
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
|
||||
</colgroup>
|
||||
@dispatchEvent('allTicketsTable.beforeHead', ['tickets' => $allTickets])
|
||||
<thead>
|
||||
@dispatchEvent('allTicketsTable.beforeHeadRow', ['tickets' => $allTickets])
|
||||
<tr>
|
||||
<th>{!! __('label.title') !!}</th>
|
||||
<th>{!! __('label.todo_type') !!}</th>
|
||||
<th>{!! __('label.progress') !!}</th>
|
||||
<th class="milestone-col">{!! __('label.dependent_on') !!}</th>
|
||||
<th>{!! __('label.todo_status') !!}</th>
|
||||
<th class="user-col">{!! __('label.owner') !!}</th>
|
||||
<th>{!! __('label.planned_start_date') !!}</th>
|
||||
<th>{!! __('label.planned_end_date') !!}</th>
|
||||
<th>{!! __('label.planned_hours') !!}</th>
|
||||
<th>{!! __('label.estimated_hours_remaining') !!}</th>
|
||||
<th>{!! __('label.booked_hours') !!}</th>
|
||||
<th class="no-sort"></th>
|
||||
</tr>
|
||||
@dispatchEvent('allTicketsTable.afterHeadRow', ['tickets' => $allTickets])
|
||||
</thead>
|
||||
@dispatchEvent('allTicketsTable.afterHead', ['tickets' => $allTickets])
|
||||
<tbody>
|
||||
@dispatchEvent('allTicketsTable.beforeFirstRow', ['tickets' => $allTickets])
|
||||
@foreach ($allTickets as $rowNum => $row)
|
||||
<tr>
|
||||
@dispatchEvent('allTicketsTable.afterRowStart', ['rowNum' => $rowNum, 'tickets' => $allTickets])
|
||||
<td data-order="{{ $row['headline'] }}">
|
||||
@if ($row['type'] == 'milestone')
|
||||
<a href="#/tickets/editMilestone/{{ $row['id'] }}">{{ $row['headline'] }}</a>
|
||||
@else
|
||||
<a href="#/tickets/showTicket/{{ $row['id'] }}">{{ $row['headline'] }}</a>
|
||||
@endif
|
||||
</td>
|
||||
<td>{!! __('label.'.strtolower($row['type'])) !!}</td>
|
||||
|
||||
<td>
|
||||
@if ($row['type'] == 'milestone')
|
||||
<div hx-trigger="load"
|
||||
hx-get="{{ BASE_URL }}/hx/tickets/milestones/progress?milestoneId={{ $row['id'] }}&view=Progress">
|
||||
<div class="htmx-indicator">
|
||||
{!! __('label.calculating_progress') !!}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@php
|
||||
if ($row['milestoneid'] != '' && $row['milestoneid'] != 0) {
|
||||
$milestoneHeadline = $tpl->escape($row['milestoneHeadline']);
|
||||
} else {
|
||||
$milestoneHeadline = __('label.no_milestone');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<td data-order="{{ $milestoneHeadline }}">
|
||||
<div class="dropdown ticketDropdown milestoneDropdown colorized show">
|
||||
<a style="background-color:{{ $tpl->escape($row['milestoneColor']) }}" class="dropdown-toggle label-default milestone" href="javascript:void(0);" role="button" id="milestoneDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $milestoneHeadline }}</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="milestoneDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_milestone') !!}</li>
|
||||
<li class='dropdown-item'><a style='background-color:#b0b0b0' href='javascript:void(0);' data-label="{!! __('label.no_milestone') !!}" data-value='{{ $row['id'].'_0_#b0b0b0' }}'> {!! __('label.no_milestone') !!} </a></li>
|
||||
@php
|
||||
foreach ($milestones as $milestone) {
|
||||
if ($milestone->id != $row['id']) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".$tpl->escape($milestone->headline)."' data-value='".$row['id'].'_'.$milestone->id.'_'.$tpl->escape($milestone->tags)."' id='ticketMilestoneChange".$row['id'].$milestone->id."' style='background-color:".$tpl->escape($milestone->tags)."'>".$tpl->escape($milestone->headline).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
@php
|
||||
if (isset($statusLabels[$row['status']])) {
|
||||
$class = $statusLabels[$row['status']]['class'];
|
||||
$name = $statusLabels[$row['status']]['name'];
|
||||
$sortKey = $statusLabels[$row['status']]['sortKey'];
|
||||
} else {
|
||||
$class = 'label-important';
|
||||
$name = 'new';
|
||||
$sortKey = 0;
|
||||
}
|
||||
@endphp
|
||||
<td data-order="{{ $sortKey }}">
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show">
|
||||
<a class="dropdown-toggle status {{ $class }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['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{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_status') !!}</li>
|
||||
@php
|
||||
foreach ($statusLabels as $key => $label) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='".$label['class']."' data-label='".$tpl->escape($label['name'])."' data-value='".$row['id'].'_'.$key.'_'.$label['class']."' id='ticketStatusChange".$row['id'].$key."' >".$tpl->escape($label['name']).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row['editorFirstname'] != '' ? $tpl->escape($row['editorFirstname']) : __('dropdown.not_assigned') }}">
|
||||
<div class="dropdown ticketDropdown userDropdown noBg show ">
|
||||
<a class="dropdown-toggle" href="javascript:void(0);" role="button" id="userDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">
|
||||
@php
|
||||
if ($row['editorFirstname'] != '') {
|
||||
echo "<span id='userImage".$row['id']."'><img src='".BASE_URL.'/api/users?profileImage='.$row['editorId']."' width='25' style='vertical-align: middle; margin-right:5px;'/></span><span id='user".$row['id']."'> ".$tpl->escape($row['editorFirstname']).'</span>';
|
||||
} else {
|
||||
echo "<span id='userImage".$row['id']."'><img src='".BASE_URL."/api/users?profileImage=false' width='25' style='vertical-align: middle; margin-right:5px;'/></span><span id='user".$row['id']."'>".__('dropdown.not_assigned').'</span>';
|
||||
}
|
||||
@endphp
|
||||
</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_user') !!}</li>
|
||||
@php
|
||||
foreach ($users as $user) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname']))."' data-value='".$row['id'].'_'.$user['id'].'_'.$user['profileId']."' id='userStatusChange".$row['id'].$user['id']."' ><img src='".BASE_URL.'/api/users?profileImage='.$user['id']."' width='25' style='vertical-align: middle; margin-right:5px;'/>".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row['editFrom'] }}" >
|
||||
{!! __('label.due_icon') !!}<input type="text" title="{{ __('label.planned_start_date') }}" value="{{ format($row['editFrom'])->date() }}" class="editFromDate secretInput milestoneEditFromAsync fromDateTicket-{{ $row['id'] }}" data-id="{{ $row['id'] }}" name="editFrom" class=""/>
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row['editTo'] }}" >
|
||||
{!! __('label.due_icon') !!}<input type="text" title="{{ __('label.planned_end_date') }}" value="{{ format($row['editTo'])->date() }}" class="editToDate secretInput milestoneEditToAsync toDateTicket-{{ $row['id'] }}" data-id="{{ $row['id'] }}" name="editTo" class="" />
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row['planHours'] }}" >
|
||||
{{ $row['planHours'] }}
|
||||
</td>
|
||||
<td data-order="{{ $row['hourRemaining'] }}" >
|
||||
{{ $row['hourRemaining'] }}
|
||||
</td>
|
||||
<td data-order="{{ $row['bookedHours'] }}" >
|
||||
{{ $row['bookedHours'] }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
@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 class="nav-header">{!! __('subtitles.todo') !!}</li>
|
||||
<li><a href="{{ BASE_URL }}/tickets/editMilestone/{{ $row['id'] }}" class='ticketModal'><i class="fa fa-edit"></i> {!! __('links.edit_milestone') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/tickets/moveTicket/{{ $row['id'] }}" class="moveTicketModal sprintModal"><i class="fa-solid fa-arrow-right-arrow-left"></i> {!! __('links.move_milestone') !!}</a></li>
|
||||
<li><a href="{{ BASE_URL }}/tickets/delMilestone/{{ $row['id'] }}" class="delete"><i class="fa fa-trash"></i> {!! __('links.delete') !!}</a></li>
|
||||
<li class="nav-header border"></li>
|
||||
<li><a href="{{ BASE_URL }}/tickets/showAll?search=true&milestone={{ $row['id'] }}">{!! __('links.view_todos') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@dispatchEvent('allTicketsTable.beforeRowEnd', ['tickets' => $allTickets, 'rowNum' => $rowNum])
|
||||
</tr>
|
||||
@endforeach
|
||||
@dispatchEvent('allTicketsTable.afterLastRow', ['tickets' => $allTickets])
|
||||
</tbody>
|
||||
@dispatchEvent('allTicketsTable.afterBody', ['tickets' => $allTickets])
|
||||
</table>
|
||||
@dispatchEvent('allTicketsTable.afterClose', ['tickets' => $allTickets])
|
||||
|
||||
@if ($group['label'] != 'all')
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ticketsController.initUserDropdown();
|
||||
leantime.ticketsController.initMilestoneDropdown();
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
leantime.ticketsController.initSprintDropdown();
|
||||
leantime.ticketsController.initMilestoneDatesAsyncUpdate();
|
||||
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
leantime.ticketsController.initMilestoneTable("{{ $searchCriteria['groupBy'] }}");
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
});
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
266
app/Domain/Tickets/Templates/showAllMilestonesOverview.blade.php
Normal file
266
app/Domain/Tickets/Templates/showAllMilestonesOverview.blade.php
Normal file
@@ -0,0 +1,266 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$todoTypeIcons = $ticketTypeIcons;
|
||||
$statusLabels = $allTicketStates;
|
||||
$numberofColumns = count($allTicketStates) - 1;
|
||||
$size = floor(100 / $numberofColumns);
|
||||
@endphp
|
||||
|
||||
@include('tickets::submodules.portfolioHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
@include('tickets::submodules.portfolioTabs')
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<form action="" method="get" id="ticketSearch">
|
||||
|
||||
@dispatchEvent('filters.afterFormOpen')
|
||||
|
||||
<input type="hidden" value="1" name="search"/>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
@dispatchEvent('filters.afterLefthandSectionOpen')
|
||||
@include('tickets::submodules.ticketNewBtn')
|
||||
@include('tickets::submodules.ticketFilter')
|
||||
@dispatchEvent('filters.beforeLefthandSectionClose')
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 center">
|
||||
@dispatchEvent('filters.afterCenterSectionOpen')
|
||||
@dispatchEvent('filters.beforeCenterSectionClose')
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="pull-right">
|
||||
@dispatchEvent('filters.afterRighthandSectionOpen')
|
||||
<div id="tableButtons" style="display:inline-block"></div>
|
||||
@dispatchEvent('filters.beforeRighthandSectionClose')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@dispatchEvent('filters.beforeFormClose')
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
</form>
|
||||
|
||||
@dispatchEvent('allTicketsTable.before', ['tickets' => $allTickets])
|
||||
|
||||
<table id="allTicketsTable" class="table table-bordered display" style="width:100%">
|
||||
<colgroup>
|
||||
<col class="con1" >
|
||||
<col class="con0">
|
||||
<col class="con1">
|
||||
<col class="con0" >
|
||||
<col class="con1">
|
||||
<col class="con0">
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
<col class="con1" >
|
||||
<col class="con0" >
|
||||
</colgroup>
|
||||
@dispatchEvent('allTicketsTable.beforeHead', ['tickets' => $allTickets])
|
||||
<thead>
|
||||
@dispatchEvent('allTicketsTable.beforeHeadRow', ['tickets' => $allTickets])
|
||||
<tr>
|
||||
<th>{!! __('label.project_name') !!}</th>
|
||||
<th>{!! __('label.title') !!}</th>
|
||||
<th class="milestone-col">{!! __('label.dependent_on') !!}</th>
|
||||
<th>{!! __('label.todo_status') !!}</th>
|
||||
<th class="user-col">{!! __('label.owner') !!}</th>
|
||||
<th>{!! __('label.planned_start_date') !!}</th>
|
||||
<th>{!! __('label.planned_end_date') !!}</th>
|
||||
<th>{!! __('label.planned_hours') !!}</th>
|
||||
<th>{!! __('label.estimated_hours_remaining') !!}</th>
|
||||
<th>{!! __('label.booked_hours') !!}</th>
|
||||
<th>{!! __('label.progress') !!}</th>
|
||||
<th class="no-sort"></th>
|
||||
</tr>
|
||||
@dispatchEvent('allTicketsTable.afterHeadRow', ['tickets' => $allTickets])
|
||||
</thead>
|
||||
@dispatchEvent('allTicketsTable.afterHead', ['tickets' => $allTickets])
|
||||
<tbody>
|
||||
@dispatchEvent('allTicketsTable.beforeFirstRow', ['tickets' => $allTickets])
|
||||
@foreach ($allTickets as $rowNum => $row)
|
||||
<tr>
|
||||
<td><h4>{{ $row->projectName }} </h4></td>
|
||||
@dispatchEvent('allTicketsTable.afterRowStart', ['rowNum' => $rowNum, 'tickets' => $allTickets])
|
||||
<td data-order="{{ $row->headline }}"><a href="#/tickets/editMilestone/{{ $row->id }}">{{ $row->headline }}</a></td>
|
||||
@php
|
||||
if ($row->milestoneid != '' && $row->milestoneid != 0) {
|
||||
$milestoneHeadline = $tpl->escape($row->milestoneHeadline);
|
||||
} else {
|
||||
$milestoneHeadline = __('label.no_milestone');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<td class="dropdown-cell" data-order="{{ $milestoneHeadline }}">
|
||||
<div class="dropdown ticketDropdown milestoneDropdown colorized show">
|
||||
<a style="background-color:{{ $tpl->escape($row->milestoneColor) }}" class="dropdown-toggle label-default milestone" href="javascript:void(0);" role="button" id="milestoneDropdownMenuLink{{ $row->id }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $milestoneHeadline }}</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="milestoneDropdownMenuLink{{ $row->id }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_milestone') !!}</li>
|
||||
<li class='dropdown-item'><a style='background-color:#b0b0b0' href='javascript:void(0);' data-label="{!! __('label.no_milestone') !!}" data-value='{{ $row->id.'_0_#b0b0b0' }}'> {!! __('label.no_milestone') !!} </a></li>
|
||||
@php
|
||||
foreach ($milestones as $milestone) {
|
||||
if ($milestone->id != $row->id) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".$tpl->escape($milestone->headline)."' data-value='".$row->id.'_'.$milestone->id.'_'.$tpl->escape($milestone->tags)."' id='ticketMilestoneChange".$row->id.$milestone->id."' style='background-color:".$tpl->escape($milestone->tags)."'>".$tpl->escape($milestone->headline).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@php
|
||||
if (isset($statusLabels[$row->status])) {
|
||||
$class = $statusLabels[$row->status]['class'];
|
||||
$name = $statusLabels[$row->status]['name'];
|
||||
} else {
|
||||
$class = 'label-important';
|
||||
$name = 'new';
|
||||
}
|
||||
@endphp
|
||||
<td class="dropdown-cell" data-order="{{ $name }}">
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show">
|
||||
<a class="dropdown-toggle status {{ $class }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row->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{{ $row->id }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_status') !!}</li>
|
||||
@php
|
||||
foreach ($statusLabels as $key => $label) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='".$label['class']."' data-label='".$tpl->escape($label['name'])."' data-value='".$row->id.'_'.$key.'_'.$label['class']."' id='ticketStatusChange".$row->id.$key."' >".$tpl->escape($label['name']).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="dropdown-cell" data-order="{{ $row->editorFirstname != '' ? $tpl->escape($row->editorFirstname) : __('dropdown.not_assigned') }}">
|
||||
<div class="dropdown ticketDropdown userDropdown noBg show ">
|
||||
<a class="dropdown-toggle" href="javascript:void(0);" role="button" id="userDropdownMenuLink{{ $row->id }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">
|
||||
@php
|
||||
if ($row->editorFirstname != '') {
|
||||
echo "<span id='userImage".$row->id."'><img src='".BASE_URL.'/api/users?profileImage='.$row->editorId."' width='25' style='vertical-align: middle; margin-right:5px;'/></span><span id='user".$row->id."'> ".$tpl->escape($row->editorFirstname).'</span>';
|
||||
} else {
|
||||
echo "<span id='userImage".$row->id."'><img src='".BASE_URL."/api/users?profileImage=false' width='25' style='vertical-align: middle; margin-right:5px;'/></span><span id='user".$row->id."'>".__('dropdown.not_assigned').'</span>';
|
||||
}
|
||||
@endphp
|
||||
</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row->id }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_user') !!}</li>
|
||||
@php
|
||||
foreach ($users as $user) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname']))."' data-value='".$row->id.'_'.$user['id'].'_'.$user['profileId']."' id='userStatusChange".$row->id.$user['id']."' ><img src='".BASE_URL.'/api/users?profileImage='.$user['id']."' width='25' style='vertical-align: middle; margin-right:5px;'/>".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row->editFrom }}" >
|
||||
{!! __('label.due_icon') !!}<input type="text" title="{{ __('label.planned_start_date') }}" value="{{ format($row->editFrom)->date() }}" class="editFromDate secretInput milestoneEditFromAsync fromDateTicket-{{ $row->id }}" data-id="{{ $row->id }}" name="editFrom" class=""/>
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row->editTo }}" >
|
||||
{!! __('label.due_icon') !!}<input type="text" title="{{ __('label.planned_end_date') }}" value="{{ format($row->editTo)->date() }}" class="editToDate secretInput milestoneEditToAsync toDateTicket-{{ $row->id }}" data-id="{{ $row->id }}" name="editTo" class="" />
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row->planHours }}" >
|
||||
{{ $row->planHours }}
|
||||
</td>
|
||||
<td data-order="{{ $row->hourRemaining }}" >
|
||||
{{ $row->hourRemaining }}
|
||||
</td>
|
||||
<td data-order="{{ $row->bookedHours }}" >
|
||||
{{ $row->bookedHours }}
|
||||
</td>
|
||||
|
||||
<td data-order="{{ $row->percentDone }}">
|
||||
<div class="progress " style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-success " role="progressbar" aria-valuenow="{{ $row->percentDone }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ $row->percentDone }}%">
|
||||
<span class="sr-only">{!! sprintf(__('text.percent_complete'), $row->percentDone) !!}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@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 class="nav-header">{!! __('subtitles.todo') !!}</li>
|
||||
<li><a href="#/tickets/editMilestone/{{ $row->id }}" class='ticketModal'><i class="fa fa-edit"></i> {!! __('links.edit_milestone') !!}</a></li>
|
||||
<li><a href="#/tickets/moveTicket/{{ $row->id }}" class="moveTicketModal sprintModal"><i class="fa-solid fa-arrow-right-arrow-left"></i> {!! __('links.move_milestone') !!}</a></li>
|
||||
<li><a href="#/tickets/delMilestone/{{ $row->id }}" class="delete"><i class="fa fa-trash"></i> {!! __('links.delete') !!}</a></li>
|
||||
<li class="nav-header border"></li>
|
||||
<li><a href="{{ BASE_URL }}/tickets/showAll?search=true&milestone={{ $row->id }}">{!! __('links.view_todos') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@dispatchEvent('allTicketsTable.beforeRowEnd', ['tickets' => $allTickets, 'rowNum' => $rowNum])
|
||||
</tr>
|
||||
@endforeach
|
||||
@dispatchEvent('allTicketsTable.afterLastRow', ['tickets' => $allTickets])
|
||||
</tbody>
|
||||
@dispatchEvent('allTicketsTable.afterBody', ['tickets' => $allTickets])
|
||||
</table>
|
||||
@dispatchEvent('allTicketsTable.afterClose', ['tickets' => $allTickets])
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
});
|
||||
|
||||
leantime.ticketsController.initTicketSearchSubmit("{{ BASE_URL }}/tickets/showAll");
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ticketsController.initUserDropdown();
|
||||
leantime.ticketsController.initMilestoneDropdown();
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
leantime.ticketsController.initSprintDropdown();
|
||||
leantime.ticketsController.initMilestoneDatesAsyncUpdate();
|
||||
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
leantime.ticketsController.initMilestoneTable("{{ $searchCriteria['groupBy'] }}");
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
482
app/Domain/Tickets/Templates/showKanban.blade.php
Normal file
482
app/Domain/Tickets/Templates/showKanban.blade.php
Normal file
@@ -0,0 +1,482 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$tickets = $tickets ?? [];
|
||||
$todoTypeIcons = $ticketTypeIcons;
|
||||
$allTicketGroups = $allTickets;
|
||||
$reopenState = session()->get('quickadd_reopen', null);
|
||||
$currentGroupBy = $searchCriteria['groupBy'] ?? 'all';
|
||||
// Program (cross-project) board: columns are semantic status types and tickets are placed
|
||||
// by their computed statusType (resolved from each project) instead of the raw status key.
|
||||
$programBoard = $programBoard ?? false;
|
||||
$placementField = $programBoard ? 'statusType' : 'status';
|
||||
@endphp
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<script>
|
||||
leantime.kanbanGroupBy = '{{ $tpl->escape($currentGroupBy) }}';
|
||||
</script>
|
||||
|
||||
@include('tickets::submodules.ticketHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
@include('tickets::submodules.ticketBoardTabs')
|
||||
|
||||
<div class="maincontentinner kanban-board-wrapper" >
|
||||
|
||||
{{-- Board actions (New / Filter / Group By) moved into the nav bar
|
||||
(ticketBoardTabs) so there's no separate toolbar row here. --}}
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@if ($programBoard)
|
||||
<p class="tw-text-[var(--secondary-font-color)]" style="margin-bottom:15px;">
|
||||
<i class="fa fa-circle-info" aria-hidden="true"></i>
|
||||
{{ __('text.program_status_rollup') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if (isset($allTicketGroups['all']))
|
||||
@php $allTickets = $allTicketGroups['all']['items']; @endphp
|
||||
@endif
|
||||
|
||||
@php
|
||||
$isGroupByActive = ! empty($searchCriteria['groupBy']) && $searchCriteria['groupBy'] !== 'all';
|
||||
$columnHeaderClass = $isGroupByActive ? 'groupby-active' : '';
|
||||
@endphp
|
||||
<div class="kanban-column-headers {{ $columnHeaderClass }}" style="
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 110px;
|
||||
justify-content: flex-start;
|
||||
z-index: 9;
|
||||
">
|
||||
@foreach ($allKanbanColumns as $key => $statusRow)
|
||||
<div class="column">
|
||||
<h4 class="widgettitle title-primary title-border-{{ $statusRow['class'] }}">
|
||||
@if ($login::userIsAtLeast($roles::$manager) && ! $programBoard)
|
||||
<div class="inlineDropDownContainer" style="float:right;">
|
||||
<a href="javascript:void(0);" class="dropdown-toggle ticketDropDown editHeadline" data-toggle="dropdown">
|
||||
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#/setting/editBoxLabel?module=ticketlabels&label={{ $key }}" class="editLabelModal">{!! __('headlines.edit_label') !!}</a>
|
||||
</li>
|
||||
<li><a href="{{ BASE_URL }}/projects/showProject/{{ session('currentProject') }}#todosettings">{!! __('links.add_remove_col') !!}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
<strong class="count">0</strong>
|
||||
{{ $statusRow['name'] }}
|
||||
</h4>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@foreach ($allTicketGroups as $group)
|
||||
@php $allTickets = $group['items']; @endphp
|
||||
|
||||
@if ($group['label'] != 'all')
|
||||
@php
|
||||
$swimlaneExpanded = ! in_array($group['id'], session('collapsedSwimlanes', []));
|
||||
$groupBy = $searchCriteria['groupBy'] ?? 'status';
|
||||
$groupId = $group['id'];
|
||||
$groupIdKey = (string) $groupId;
|
||||
$swimlaneBreakdown = $statusBreakdown[$groupIdKey] ?? $statusBreakdown[$groupId] ?? [];
|
||||
$statusCounts = $swimlaneBreakdown['statusCounts'] ?? [];
|
||||
$timeAlert = $swimlaneBreakdown['timeAlert'] ?? null;
|
||||
@endphp
|
||||
<div class="kanban-swimlane-row" data-expanded="{{ $swimlaneExpanded ? 'true' : 'false' }}" id="swimlane-row-{{ $group['id'] }}">
|
||||
<div class="kanban-swimlane-sentinel" data-swimlane-id="{{ $group['id'] }}" aria-hidden="true"></div>
|
||||
|
||||
{{-- Render the component directly. It was previously invoked via
|
||||
app('blade.compiler')::render('<x-...>', $data), but a <x-component> string
|
||||
passed to Blade::render from inside an already-compiled view gets its bare
|
||||
variables ($label, $groupId, …) pre-compiled by the outer pass and they are
|
||||
not in the inner render scope — throwing "Undefined variable" for ANY
|
||||
kanban group-by. A plain component tag with inline expressions is correct. --}}
|
||||
<x-global::kanban.swimlane-row-header
|
||||
:groupBy="$groupBy"
|
||||
:groupId="$group['id']"
|
||||
:label="$group['label']"
|
||||
:totalCount="$swimlaneBreakdown['totalCount'] ?? count($group['items'])"
|
||||
:statusCounts="$statusCounts"
|
||||
:statusColumns="$allKanbanColumns"
|
||||
:expanded="$swimlaneExpanded"
|
||||
:moreInfo="$group['more-info'] ?? null"
|
||||
:timeAlert="$group['timeAlert'] ?? null"
|
||||
/>
|
||||
|
||||
<div class="kanban-swimlane-content{{ !$swimlaneExpanded ? ' collapsed' : '' }}" id="swimlane-content-{{ $group['id'] }}">
|
||||
@endif
|
||||
|
||||
<div class="sortableTicketList kanbanBoard" id="kanboard-{{ $group['id'] }}" style="margin-top:-5px;">
|
||||
|
||||
<div class="row-fluid">
|
||||
|
||||
@php
|
||||
$emptyColumns = [];
|
||||
foreach ($allKanbanColumns as $key => $statusRow) {
|
||||
$hasTickets = false;
|
||||
if (isset($allTickets)) {
|
||||
foreach ($allTickets as $ticket) {
|
||||
if (isset($ticket[$placementField]) && $ticket[$placementField] == $key) {
|
||||
$hasTickets = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! $hasTickets) {
|
||||
$emptyColumns[$key] = true;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@foreach ($allKanbanColumns as $key => $statusRow)
|
||||
<div class="column">
|
||||
<div class="contentInner status_{{ $key }} {{ isset($emptyColumns[$key]) ? 'empty-column' : '' }}"
|
||||
data-empty-text="{{ isset($emptyColumns[$key]) ? 'Empty' : '' }}"
|
||||
aria-label="{{ isset($emptyColumns[$key]) ? 'Empty column' : htmlspecialchars($statusRow['name']).' column items' }}"
|
||||
role="list">
|
||||
|
||||
@include('tickets::partials.quickadd-form', [
|
||||
'statusId' => $key,
|
||||
'swimlaneKey' => $group['value'] ?? $group['id'] ?? null,
|
||||
'isEmpty' => isset($emptyColumns[$key]),
|
||||
'currentGroupBy' => $searchCriteria['groupBy'] ?? null,
|
||||
'programBoard' => $programBoard,
|
||||
'availableProjects' => $availableProjects ?? null,
|
||||
])
|
||||
|
||||
@foreach ($allTickets as $row)
|
||||
@if (($row[$placementField] ?? null) == $key)
|
||||
<div class="ticketBox moveable container priority-border-{{ $row['priority'] }}" id="ticket_{{ $row['id'] }}">
|
||||
|
||||
<div class="row" >
|
||||
<div class="col-md-12">
|
||||
|
||||
@include('tickets::partials.ticketsubmenu', ['ticket' => $row, 'onTheClock' => $onTheClock])
|
||||
|
||||
@if ($row['dependingTicketId'] > 0)
|
||||
<small><a href="#/tickets/showTicket/{{ $row['dependingTicketId'] }}" class="form-modal">{{ $row['parentHeadline'] }}</a></small> //
|
||||
@endif
|
||||
<small><i class="fa {{ $todoTypeIcons[strtolower($row['type'])] }}"></i> {!! __('label.'.strtolower($row['type'])) !!}</small>
|
||||
<small>#{{ $row['id'] }}</small>
|
||||
<div class="kanbanCardContent">
|
||||
<h4><a href="#/tickets/showTicket/{{ $row['id'] }}" data-hx-get="{{ BASE_URL }}/tickets/showTicket/{{ $row['id'] }}" hx-swap="none" preload="mouseover">{{ $row['headline'] }}</a></h4>
|
||||
|
||||
<div class="kanbanContent" style="margin-bottom: 20px">
|
||||
{!! $tpl->escapeMinimal($row['description']) !!}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tw-flex">
|
||||
@if ($row['dateToFinish'] != '0000-00-00 00:00:00' && $row['dateToFinish'] != '1969-12-31 00:00:00')
|
||||
<div>
|
||||
{!! __('label.due_icon') !!}
|
||||
<input type="text" title="{{ __('label.due') }}" value="{{ format($row['dateToFinish'])->date() }}" class="duedates secretInput" style="margin-left:0px;" data-id="{{ $row['id'] }}" name="date" />
|
||||
</div>
|
||||
<div>
|
||||
@dispatchEvent('afterDates', ['ticket' => $row])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix" style="padding-bottom: 8px;"></div>
|
||||
|
||||
<div class="timerContainer " id="timerContainer-{{ $row['id'] }}" >
|
||||
|
||||
<div class="dropdown ticketDropdown milestoneDropdown colorized show firstDropdown" >
|
||||
<a style="background-color:{{ $tpl->escape($row['milestoneColor']) }}" class="dropdown-toggle f-left label-default milestone" href="javascript:void(0);" role="button" id="milestoneDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">@if ($row['milestoneid'] != '' && $row['milestoneid'] != 0){{ $row['milestoneHeadline'] }}@else{!! __('label.no_milestone') !!}@endif</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="milestoneDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_milestone') !!}</li>
|
||||
<li class='dropdown-item'><a style='background-color:#b0b0b0' href='javascript:void(0);' data-label="{!! __('label.no_milestone') !!}" data-value='{{ $row['id'].'_0_#b0b0b0' }}'> {!! __('label.no_milestone') !!} </a></li>
|
||||
|
||||
@php
|
||||
foreach ($milestones as $milestone) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".$tpl->escape($milestone->headline)."' data-value='".$row['id'].'_'.$milestone->id.'_'.$tpl->escape($milestone->tags)."' id='ticketMilestoneChange".$row['id'].$milestone->id."' style='background-color:".$tpl->escape($milestone->tags)."'>".$tpl->escape($milestone->headline).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@if ($row['storypoints'] != '' && $row['storypoints'] > 0)
|
||||
<div class="dropdown ticketDropdown effortDropdown show">
|
||||
<a class="dropdown-toggle f-left label-default effort" href="javascript:void(0);" role="button" id="effortDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $efforts[''.$row['storypoints']] ?? $row['storypoints'] }}</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="effortDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.how_big_todo') !!}</li>
|
||||
@php
|
||||
foreach ($efforts as $effortKey => $effortValue) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-value='".$row['id'].'_'.$effortKey."' id='ticketEffortChange".$row['id'].$effortKey."'>".$effortValue.'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<div class="dropdown ticketDropdown priorityDropdown show">
|
||||
<a class="dropdown-toggle f-left label-default priority priority-bg-{{ $row['priority'] }}" href="javascript:void(0);" role="button" id="priorityDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">@php if ($row['priority'] != '' && $row['priority'] > 0) { echo $priorities[$row['priority']] ?? __('label.priority_unkown'); } else { echo __('label.priority_unkown'); } @endphp</span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="priorityDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.select_priority') !!}</li>
|
||||
@php
|
||||
foreach ($priorities as $priorityKey => $priorityValue) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='priority-bg-".$priorityKey."' data-value='".$row['id'].'_'.$priorityKey."' id='ticketPriorityChange".$row['id'].$priorityKey."'>".$priorityValue.'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="dropdown ticketDropdown userDropdown noBg show right lastDropdown dropRight">
|
||||
<a class="dropdown-toggle f-left" href="javascript:void(0);" role="button" id="userDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text" style="display:inline-flex; align-items:center;">
|
||||
@php
|
||||
if ($row['editorFirstname'] != '') {
|
||||
echo "<span id='userImage".$row['id']."'><img src='".BASE_URL.'/api/users?profileImage='.$row['editorId']."' width='25' style='vertical-align: middle;'/></span>";
|
||||
} else {
|
||||
echo "<span id='userImage".$row['id']."'><img src='".BASE_URL."/api/users?profileImage=false' width='25' style='vertical-align: middle;'/></span>";
|
||||
}
|
||||
|
||||
if (! empty($row['collaboratorPreview'])) {
|
||||
echo "<span class='ticket-collaborators' style='display:inline-flex; align-items:center; margin-left:6px;'>";
|
||||
foreach ($row['collaboratorPreview'] as $index => $collaboratorId) {
|
||||
$offset = $index > 0 ? 'margin-left:-8px;' : '';
|
||||
echo "<span class='ticket-collaborator-avatar' title='".__('label.collaborators')."' style='display:inline-flex; width:18px; height:18px; border-radius:999px; border:2px solid var(--main-background-color, #fff); overflow:hidden; ".$offset."'><img src='".BASE_URL.'/api/users?profileImage='.$collaboratorId."' width='18' height='18' style='display:block; width:18px; height:18px;'/></span>";
|
||||
}
|
||||
if (($row['collaboratorOverflow'] ?? 0) > 0) {
|
||||
echo "<span class='ticket-collaborator-more' title='".__('label.collaborators')."' style='display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 4px; margin-left:4px; border-radius:999px; background:var(--accent-color, #e9ecef); color:var(--secondary-font-color, #333); font-size:10px; line-height:18px;'>+".(int) $row['collaboratorOverflow'].'</span>';
|
||||
}
|
||||
echo '</span>';
|
||||
}
|
||||
@endphp
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_user') !!}</li>
|
||||
@php
|
||||
if (is_array($users)) {
|
||||
foreach ($users as $user) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' data-label='".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname']))."' data-value='".$row['id'].'_'.$user['id'].'_'.$user['profileId']."' id='userStatusChange".$row['id'].$user['id']."' ><img src='".BASE_URL.'/api/users?profileImage='.$user['id']."' width='25' style='vertical-align: middle; margin-right:5px;'/>".sprintf(__('text.full_name'), $tpl->escape($user['firstname']), $tpl->escape($user['lastname'])).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@if ($programBoard)
|
||||
{{-- Cross-project board: columns are semantic stages, so give each card a
|
||||
dropdown of its OWN project's real statuses (e.g. "Blocked") to set the
|
||||
detailed status directly. patchTicket writes a key valid in that project,
|
||||
so it stays orphan-safe. Also show which project the task belongs to. --}}
|
||||
@php $rowProjectStatuses = $statusLabelsByProject[$row['projectId']] ?? []; @endphp
|
||||
@php $rowProjectStatus = $rowProjectStatuses[$row['status']] ?? null; @endphp
|
||||
<div style="margin-top:4px;">
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show" style="display:inline-block;">
|
||||
<a class="dropdown-toggle status {{ $rowProjectStatus['class'] ?? 'label-default' }} f-left" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="text">{{ $tpl->escape($rowProjectStatus['name'] ?? __('label.new')) }}</span> <i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_status') !!}</li>
|
||||
@php
|
||||
foreach ($rowProjectStatuses as $statusKey => $statusOption) {
|
||||
echo "<li class='dropdown-item'><a href='javascript:void(0);' class='".$statusOption['class']."' data-label='".$tpl->escape($statusOption['name'])."' data-value='".$row['id'].'_'.$statusKey.'_'.$statusOption['class']."' id='ticketStatusChange".$row['id'].$statusKey."'>".$tpl->escape($statusOption['name']).'</a></li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
<small class="tw-text-[var(--secondary-font-color)]">{{ $tpl->escape($row['projectName'] ?? '') }}</small>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($row['commentCount'] > 0 || $row['subtaskCount'] > 0 || $row['tags'] != '')
|
||||
<div class="row">
|
||||
<div class="col-md-12 border-top" style="white-space: nowrap;">
|
||||
@if ($row['commentCount'] > 0)
|
||||
<a href="#/tickets/showTicket/{{ $row['id'] }}"><span class="fa-regular fa-comments"></span> {{ $row['commentCount'] }}</a>
|
||||
@endif
|
||||
|
||||
@if ($row['subtaskCount'] > 0)
|
||||
<a id="subtaskLink_{{ $row['id'] }}" href="#/tickets/showTicket/{{ $row['id'] }}" class="subtaskLineLink"> <span class="fa fa-diagram-successor"></span> {{ $row['subtaskCount'] }}</a>
|
||||
@endif
|
||||
@if ($row['tags'] != '')
|
||||
@php $tagsArray = explode(',', $row['tags']); @endphp
|
||||
<a href="javascript:void(0);" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-tags" aria-hidden="true"></i> {{ count($tagsArray) }}
|
||||
</a>
|
||||
<ul class="dropdown-menu ">
|
||||
<li style="padding:10px"><div class='tagsinput readonly'>
|
||||
@foreach ($tagsArray as $tag)
|
||||
<span class='tag'><span>{{ $tag }}</span></span>
|
||||
@endforeach
|
||||
</div></li></ul>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="clearfix"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($group['label'] != 'all')
|
||||
</div> {{-- .kanban-swimlane-content --}}
|
||||
</div> {{-- .kanban-swimlane-row --}}
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
@if (can('tickets.edit'))
|
||||
leantime.ticketsController.initUserDropdown();
|
||||
leantime.ticketsController.initMilestoneDropdown();
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initPriorityDropdown();
|
||||
|
||||
@if ($programBoard)
|
||||
{{-- Per-card status dropdown (program board only): set the exact project status. --}}
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
@endif
|
||||
|
||||
|
||||
@if ($programBoard)
|
||||
{{-- Program board: columns are status types; drag persists per-project via the plugin. --}}
|
||||
var ticketStatusList = [@foreach ($allKanbanColumns as $key => $statusRow)'{{ $key }}',@endforeach];
|
||||
if (leantime.pgmProBoard && typeof leantime.pgmProBoard.initProgramKanban === 'function') {
|
||||
leantime.pgmProBoard.initProgramKanban(ticketStatusList);
|
||||
} else {
|
||||
console.warn('PgmPro board JS is not loaded; program kanban drag-and-drop is disabled.');
|
||||
}
|
||||
@else
|
||||
var ticketStatusList = [@foreach ($allTicketStates as $key => $statusRow)'{{ $key }}',@endforeach];
|
||||
leantime.ticketsController.initTicketKanban(ticketStatusList);
|
||||
@endif
|
||||
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
leantime.ticketsController.setUpKanbanColumns();
|
||||
|
||||
@if (isset($_GET['showTicketModal']))
|
||||
@php
|
||||
$modalUrl = $_GET['showTicketModal'] == '' ? '' : '/'.(int) $_GET['showTicketModal'];
|
||||
@endphp
|
||||
|
||||
leantime.ticketsController.openTicketModalManually("{{ BASE_URL }}/tickets/showTicket{{ $modalUrl }}");
|
||||
window.history.pushState({},document.title, '{{ BASE_URL }}/tickets/showKanban');
|
||||
|
||||
@endif
|
||||
|
||||
|
||||
@php
|
||||
foreach ($allTicketGroups as $group) {
|
||||
foreach ($group['items'] as $ticket) {
|
||||
if ($ticket['dependingTicketId'] > 0) {
|
||||
@endphp
|
||||
var startElement = document.getElementById('subtaskLink_{{ $ticket['dependingTicketId'] }}');
|
||||
var endElement = document.getElementById('ticket_{{ $ticket['id'] }}');
|
||||
|
||||
|
||||
if ( startElement != undefined && endElement != undefined) {
|
||||
|
||||
var startAnchor = LeaderLine.mouseHoverAnchor({
|
||||
element: startElement,
|
||||
showEffectName: 'draw',
|
||||
style: {background: 'none', backgroundColor: 'none'},
|
||||
hoverStyle: {background: 'none', backgroundColor: 'none', cursor: 'pointer'}
|
||||
});
|
||||
|
||||
var line{{ $ticket['id'] }} = new LeaderLine(startAnchor, endElement, {
|
||||
startPlugColor: 'var(--accent1)',
|
||||
endPlugColor: 'var(--accent2)',
|
||||
gradient: true,
|
||||
size: 2,
|
||||
path: "grid",
|
||||
startSocket: 'bottom',
|
||||
endSocket: 'auto'
|
||||
});
|
||||
|
||||
jQuery("#ticket_{{ $ticket['id'] }}").mousedown(function () {
|
||||
|
||||
})
|
||||
.mousemove(function () {
|
||||
|
||||
})
|
||||
.mouseup(function () {
|
||||
line{{ $ticket['id'] }}.position();
|
||||
});
|
||||
|
||||
jQuery("#ticket_{{ $ticket['dependingTicketId'] }}").mousedown(function () {
|
||||
|
||||
})
|
||||
.mousemove(function () {
|
||||
|
||||
|
||||
})
|
||||
.mouseup(function () {
|
||||
line{{ $ticket['id'] }}.position();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@php
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
169
app/Domain/Tickets/Templates/showList.blade.php
Normal file
169
app/Domain/Tickets/Templates/showList.blade.php
Normal file
@@ -0,0 +1,169 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$allTicketGroups = $allTickets;
|
||||
$statusLabels = $allTicketStates;
|
||||
$groupBy = $groupBy ?? [];
|
||||
$newField = $newField ?? [];
|
||||
$numberofColumns = count($allTicketStates) - 1;
|
||||
$size = floor(100 / $numberofColumns);
|
||||
@endphp
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
@include('tickets::submodules.ticketHeader')
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
@include('tickets::submodules.ticketBoardTabs')
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
{{-- Board actions (New / Filter / Group By) moved into the nav bar
|
||||
(ticketBoardTabs) so there's no separate toolbar row here. --}}
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@dispatchEvent('allTicketsTable.before', ['tickets' => $allTickets])
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="quickAddForm" style="margin-top:15px;">
|
||||
<form action="" method="post">
|
||||
<x-global::forms.text-input name="headline" autofocus placeholder="{{ __('input.placeholders.create_task') }}" style="width: 100%;" />
|
||||
@if (isset($availableProjects))
|
||||
{{-- Program (cross-project) board: a task must belong to one child project. --}}
|
||||
<select name="quickaddProjectId" class="form-control tw-mb-s" required aria-label="{{ __('label.project') }}">
|
||||
<option value="">{{ __('label.project') }}…</option>
|
||||
@foreach ($availableProjects as $quickAddProjectId => $quickAddProjectName)
|
||||
<option value="{{ $quickAddProjectId }}">{{ $tpl->escape($quickAddProjectName) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
<input type="hidden" name="sprint" value="{{ $currentSprint }}" />
|
||||
<input type="hidden" name="milestone" value="{{ htmlspecialchars((string) ($searchCriteria['milestone'] ?? ''), ENT_QUOTES, 'UTF-8') }}" />
|
||||
<input type="hidden" name="groupBy" value="{{ htmlspecialchars((string) ($searchCriteria['groupBy'] ?? ''), ENT_QUOTES, 'UTF-8') }}" />
|
||||
<input type="hidden" name="quickadd" value="1"/>
|
||||
<x-global::forms.button tag="input" inputType="submit" class="tw-mb-m" contentRole="primary" :labelText="__('buttons.save')" name="saveTicket" style="vertical-align: top; " />
|
||||
</form>
|
||||
|
||||
|
||||
@foreach ($allTicketGroups as $group)
|
||||
@if ($group['label'] != 'all')
|
||||
<h5 class="accordionTitle {{ $group['class'] }}" @if (!empty($group['color'])) style="color:{{ htmlspecialchars($group['color']) }}" @endif id="accordion_link_{{ $group['id'] }}">
|
||||
<a href="javascript:void(0)" class="accordion-toggle" id="accordion_toggle_{{ $group['id'] }}" onclick="leantime.snippets.accordionToggle('{{ $group['id'] }}');">
|
||||
<i class="fa fa-angle-down"></i>{{ $group['label'] }} ({{ count($group['items']) }})
|
||||
</a>
|
||||
</h5>
|
||||
<div class="simpleAccordionContainer" id="accordion_content-{{ $group['id'] }}">
|
||||
@endif
|
||||
|
||||
@php $allTickets = $group['items']; @endphp
|
||||
|
||||
|
||||
<table class="table display listStyleTable" style="width:100%">
|
||||
|
||||
@dispatchEvent('allTicketsTable.beforeHead', ['tickets' => $allTickets])
|
||||
<thead>
|
||||
@dispatchEvent('allTicketsTable.beforeHeadRow', ['tickets' => $allTickets])
|
||||
<tr style="display:none;">
|
||||
|
||||
<th style="width:20px" class="status-col">{!! __('label.todo_status') !!}</th>
|
||||
<th>{!! __('label.title') !!}</th>
|
||||
</tr>
|
||||
|
||||
@dispatchEvent('allTicketsTable.afterHeadRow', ['tickets' => $allTickets])
|
||||
</thead>
|
||||
|
||||
@dispatchEvent('allTicketsTable.afterHead', ['tickets' => $allTickets])
|
||||
<tbody>
|
||||
@dispatchEvent('allTicketsTable.beforeFirstRow', ['tickets' => $allTickets])
|
||||
@foreach ($allTickets as $rowNum => $row)
|
||||
<tr onclick="leantime.ticketsController.loadTicketToContainer('{{ $row['id'] }}', '#ticketContent')" id="row-{{ $row['id'] }}" class="ticketRows">
|
||||
@dispatchEvent('allTicketsTable.afterRowStart', ['rowNum' => $rowNum, 'tickets' => $allTickets])
|
||||
@php
|
||||
// Program (cross-project) board: render/edit each row with its own
|
||||
// project's statuses so a status change never orphans the task.
|
||||
$rowStatusLabels = (isset($statusLabelsByProject) && isset($statusLabelsByProject[$row['projectId']]))
|
||||
? $statusLabelsByProject[$row['projectId']]
|
||||
: $statusLabels;
|
||||
@endphp
|
||||
<td data-order="{{ isset($rowStatusLabels[$row['status']]) ? $rowStatusLabels[$row['status']]['sortKey'] : '' }}" data-search="{{ isset($rowStatusLabels[$row['status']]) ? $rowStatusLabels[$row['status']]['name'] : '' }}" class="roundStatusBtn" style="width:20px">
|
||||
<div class="dropdown ticketDropdown statusDropdown colorized show">
|
||||
<a class="dropdown-toggle status {{ isset($rowStatusLabels[$row['status']]) ? $rowStatusLabels[$row['status']]['class'] : '' }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{{ $row['id'] }}">
|
||||
<li class="nav-header border">{!! __('dropdown.choose_status') !!}</li>
|
||||
@php
|
||||
foreach ($rowStatusLabels as $key => $label) {
|
||||
echo "<li class='dropdown-item'>
|
||||
<a href='javascript:void(0);' class='".$label['class']."' data-label='".$tpl->escape($label['name'])."' data-value='".$row['id'].'_'.$key.'_'.$label['class']."' id='ticketStatusChange".$row['id'].$key."' >".$tpl->escape($label['name']).'</a>';
|
||||
echo '</li>';
|
||||
}
|
||||
@endphp
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-search="{{ isset($rowStatusLabels[$row['status']]) ? $rowStatusLabels[$row['status']]['name'] : '' }}" data-order="{{ $row['headline'] }}" >
|
||||
<a href="javascript:void(0);"><strong>{{ $row['headline'] }}</strong></a></td>
|
||||
|
||||
@dispatchEvent('allTicketsTable.beforeRowEnd', ['tickets' => $allTickets, 'rowNum' => $rowNum])
|
||||
</tr>
|
||||
@endforeach
|
||||
@dispatchEvent('allTicketsTable.afterLastRow', ['tickets' => $allTickets])
|
||||
</tbody>
|
||||
@dispatchEvent('allTicketsTable.afterBody', ['tickets' => $allTickets])
|
||||
</table>
|
||||
|
||||
@if ($group['label'] != 'all')
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9 hidden-sm" >
|
||||
<div id="ticketContent">
|
||||
<div class="center">
|
||||
<div class='svgContainer'>
|
||||
{!! file_get_contents(ROOT.'/dist/images/svg/undraw_design_data_khdb.svg') !!}
|
||||
</div>
|
||||
|
||||
<h3>{!! __('headlines.pick_a_task') !!}</h3>
|
||||
{!! __('text.edit_tasks_in_here') !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@dispatchEvent('allTicketsTable.afterClose', ['tickets' => $allTickets])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function() {
|
||||
@dispatchEvent('scripts.afterOpen')
|
||||
|
||||
|
||||
@if ($login::userIsAtLeast($roles::$editor))
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
@else
|
||||
leantime.authController.makeInputReadonly(".maincontentinner");
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
leantime.ticketsController.initTicketsList("{{ $searchCriteria['groupBy'] }}");
|
||||
|
||||
@dispatchEvent('scripts.beforeClose')
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
90
app/Domain/Tickets/Templates/showTicket.blade.php
Normal file
90
app/Domain/Tickets/Templates/showTicket.blade.php
Normal file
@@ -0,0 +1,90 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
@php
|
||||
$projectData = $projectData ?? [];
|
||||
@endphp
|
||||
|
||||
<div class="pageheader">
|
||||
|
||||
<div class="pull-right padding-top">
|
||||
<a href="{{ session('lastPage') }}" class="backBtn"><i class="far fa-arrow-alt-circle-left"></i> {!! __('links.go_back') !!}</a>
|
||||
</div>
|
||||
|
||||
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h5>{{ session('currentProjectClient').' // '.session('currentProjectName') }}</h5>
|
||||
<h1>{!! __('headlines.edit_todo') !!}</h1>
|
||||
</div>
|
||||
|
||||
</div><!--pageheader-->
|
||||
|
||||
<div class="maincontent">
|
||||
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="tabbedwidget tab-primary ticketTabs" style="visibility:hidden;">
|
||||
|
||||
<ul>
|
||||
<li><a href="#ticketdetails">{!! __('tabs.ticketDetails') !!}</a></li>
|
||||
<li><a href="#subtasks">{!! __('tabs.subtasks') !!} ({{ $numSubTasks }})</a></li>
|
||||
<li><a href="#files">{!! __('tabs.files') !!} ({{ $numFiles }})</a></li>
|
||||
@if (session('userdata.role') != 'client')
|
||||
<li><a href="#timesheet" id="timesheetTab">{!! __('tabs.time_tracking') !!}</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
|
||||
<div id="ticketdetails">
|
||||
<form class="formModal" action="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}" method="post">
|
||||
@include('tickets::submodules.ticketDetails')
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="subtasks">
|
||||
|
||||
@include('tickets::submodules.subTasks')
|
||||
|
||||
</div>
|
||||
|
||||
<div id="files">
|
||||
<form action='#files' method='POST' enctype="multipart/form-data" class="formModal">
|
||||
@include('tickets::submodules.attachments')
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@if (session('userdata.role') != 'client')
|
||||
<div id="timesheet">
|
||||
@include('tickets::submodules.timesheet')
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="maincontentinner">
|
||||
<form method="post" action="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}#comments" class="formModal">
|
||||
<input type="hidden" name="comment" value="1" />
|
||||
@include('comments::submodules.generalComment', ['formUrl' => BASE_URL.'/tickets/showTicket/'.$ticket->id])
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@once @push('scripts')
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(window).load(function () {
|
||||
leantime.ticketsController.initTicketTabs();
|
||||
|
||||
jQuery(window).resize();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush @endonce
|
||||
|
||||
@endsection
|
||||
150
app/Domain/Tickets/Templates/showTicketModal.blade.php
Normal file
150
app/Domain/Tickets/Templates/showTicketModal.blade.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
foreach ($__data as $var => $val) {
|
||||
$$var = $val; // necessary for blade refactor
|
||||
}
|
||||
$ticket = $ticket ?? null;
|
||||
$projectData = $projectData ?? [];
|
||||
$todoTypeIcons = $ticketTypeIcons ?? [];
|
||||
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.onload = function() {
|
||||
if (!window.jQuery) {
|
||||
//It's not a modal
|
||||
location.href="<?= BASE_URL ?>/tickets/showKanban?showTicketModal=<?php echo $ticket->id; ?>";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="min-width:70%">
|
||||
|
||||
<?php if ($ticket->dependingTicketId > 0) { ?>
|
||||
<small><a href="#/tickets/showTicket/<?= $ticket->dependingTicketId ?>"><?= $tpl->escape($ticket->parentHeadline) ?></a></small> //
|
||||
<?php } ?>
|
||||
<small class="tw-float-right tw-pr-md" style="padding:5px 30px 0px 0px">Created by <?php $tpl->e($ticket->userFirstname); ?> <?php $tpl->e($ticket->userLastname); ?> | Last Updated: <?= format($ticket->date)->date(); ?> </small>
|
||||
<h1 class="tw-mb-0" style="margin-bottom:0px;"><i class="fa <?php echo $todoTypeIcons[strtolower($ticket->type)] ?? 'fa-circle'; ?>"></i> #<?= $ticket->id ?> - <?php $tpl->e($ticket->headline); ?></h1>
|
||||
|
||||
<br />
|
||||
|
||||
<?php if ($login::userIsAtLeast($roles::$editor)) {
|
||||
$onTheClock = $onTheClock ?? false;
|
||||
?>
|
||||
<div class="inlineDropDownContainer" style="float:right; z-index:50; padding-top:10px; padding-right:10px;">
|
||||
|
||||
<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"><?php echo $tpl->__('subtitles.todo'); ?></li>
|
||||
<li><a href="#/tickets/moveTicket/<?php echo $ticket->id; ?>" class="moveTicketModal sprintModal ticketModal"><i class="fa-solid fa-arrow-right-arrow-left"></i> <?php echo $tpl->__('links.move_todo'); ?></a></li>
|
||||
<li><a href="#/tickets/delTicket/<?php echo $ticket->id; ?>" class="delete"><i class="fa fa-trash"></i> <?php echo $tpl->__('links.delete_todo'); ?></a></li>
|
||||
<li class="nav-header border"><?php echo $tpl->__('subtitles.track_time'); ?></li>
|
||||
<li id="timerContainer-ticketDetails-{{ $ticket->id }}"
|
||||
hx-get="{{BASE_URL}}/tickets/timerButton/get-status/{{ $ticket->id }}"
|
||||
hx-trigger="timerUpdate from:body"
|
||||
hx-swap="outerHTML"
|
||||
class="timerContainer">
|
||||
|
||||
@if ($onTheClock === false)
|
||||
<a href="javascript:void(0);" data-value="{{ $ticket->id }}"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/start-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"ticketId": "{{ $ticket->id }}", "action":"start"}'>
|
||||
<span class="fa-regular fa-clock"></span> {{ __("links.start_work") }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($onTheClock !== false && $onTheClock["id"] == $ticket->id)
|
||||
<a href="javascript:void(0);" data-value="{{ $ticket->id }}"
|
||||
hx-patch="{{ BASE_URL }}/hx/timesheets/stopwatch/stop-timer/"
|
||||
hx-target="#timerHeadMenu"
|
||||
hx-vals='{"ticketId": "{{ $ticket->id }}", "action":"stop"}'
|
||||
hx-swap="outerHTML">
|
||||
<span class="fa fa-stop"></span>
|
||||
|
||||
@if (is_array($onTheClock) == true)
|
||||
{!! sprintf(__("links.stop_work_started_at"), date(__("language.timeformat"), $onTheClock["since"])) !!}
|
||||
@else
|
||||
{!! sprintf(__("links.stop_work_started_at"), date(__("language.timeformat"), time())) !!}
|
||||
@endif
|
||||
</a>
|
||||
@endif
|
||||
@if ($onTheClock !== false && $onTheClock["id"] != $ticket->id)
|
||||
<span class='working'>
|
||||
{{ __("text.timer_set_other_todo") }}
|
||||
</span>
|
||||
@endif
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="tabbedwidget tab-primary ticketTabs" style="visibility:hidden;">
|
||||
|
||||
<ul>
|
||||
<li><a href="#ticketdetails"><span class="fa fa-star"></span> <?php echo $tpl->__('tabs.ticketDetails') ?></a></li>
|
||||
<li><a href="#files"><span class="fa fa-file"></span> <?php echo $tpl->__('tabs.files') ?> (<?php echo $numFiles; ?>)</a></li>
|
||||
<?php if ($login::userIsAtLeast($roles::$editor)) { ?>
|
||||
<li><a href="#timesheet"><span class="fa fa-clock"></span> <?php echo $tpl->__('tabs.time_tracking') ?></a></li>
|
||||
<?php } ?>
|
||||
<?php $tpl->dispatchTplEvent('ticketTabs', ['ticket' => $ticket]); ?>
|
||||
</ul>
|
||||
|
||||
<div id="ticketdetails">
|
||||
<form class="formModal" action="{{ BASE_URL }}/tickets/showTicket/{{ $ticket->id }}" method="post">
|
||||
@include('tickets::submodules.ticketDetails')
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="files">
|
||||
@include('files::submodules.showAll')
|
||||
</div>
|
||||
|
||||
@if($login::userIsAtLeast($roles::$editor))
|
||||
<div id="timesheet">
|
||||
@include('tickets::submodules.timesheet')
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@dispatchEvent('ticketTabsContent', ['ticket' => $ticket])
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(document).ready(function(){
|
||||
|
||||
<?php if (isset($_GET['closeModal'])) { ?>
|
||||
jQuery.nmTop().close();
|
||||
<?php } ?>
|
||||
|
||||
leantime.ticketsController.initTicketTabs();
|
||||
|
||||
<?php if ($login::userIsAtLeast($roles::$editor)) { ?>
|
||||
leantime.ticketsController.initAsyncInputChange();
|
||||
leantime.ticketsController.initDueDateTimePickers();
|
||||
|
||||
leantime.dateController.initDatePicker(".dates");
|
||||
leantime.dateController.initDateRangePicker(".editFrom", ".editTo");
|
||||
|
||||
leantime.ticketsController.initTagsInput();
|
||||
|
||||
leantime.ticketsController.initEffortDropdown();
|
||||
leantime.ticketsController.initStatusDropdown();
|
||||
|
||||
jQuery(".ticketTabs select").chosen();
|
||||
|
||||
<?php } else { ?>
|
||||
leantime.authController.makeInputReadonly(".nyroModalCont");
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($login::userHasRole([$roles::$commenter])) { ?>
|
||||
leantime.commentsController.enableCommenterForms();
|
||||
<?php }?>
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -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