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

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

View File

@@ -0,0 +1,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

View 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>

View 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>

View 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

View 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>

View 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>

View 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

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>
&nbsp;<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="effortDropdownMenuLink{{ $subticket['id'] }}">
<li class="nav-header border">{{ __("dropdown.how_big_todo") }}</li>
@foreach($efforts as $effortKey => $effortValue)
<li class='dropdown-item'>
<a href='javascript:void(0);' data-value='{{ $subticket['id'] }}_{{ $effortKey }}' id='ticketEffortChange{{ $subticket['id'] . $effortKey }}'> {{ $effortValue }}</a>
</li>
@endforeach
</ul>
</div>
@php
if (isset($statusLabels[$subticket['status']])) {
$class = $statusLabels[$subticket['status']]["class"];
$name = $statusLabels[$subticket['status']]["name"];
} else {
$class = 'label-important';
$name = 'new';
}
@endphp
<div class="dropdown ticketDropdown statusDropdown colorized show">
<a class="dropdown-toggle f-left status {{ $class }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $subticket['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="text">{{$name }}
</span>
&nbsp;<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{{ $subticket['id'] }}">
<li class="nav-header border">{{ __('dropdown.choose_status') }}</li>
@foreach ($statusLabels as $key => $label)
<li class='dropdown-item'>
<a href='javascript:void(0);' class='{{ $label["class"] }}' data-label='{{ $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>

View 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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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>

View File

@@ -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

View 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>

View 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>

View 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

View 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

View 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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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

View 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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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

View 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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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

View 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>
&nbsp;<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>
&nbsp;<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>
&nbsp;<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>&nbsp;<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>&nbsp;
@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>&nbsp;
@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

View 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

View 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

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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