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,34 @@
<div class="grid-stack-item" {{ $attributes }}>
<div class="grid-stack-item-content {{ ($background == "default") ? "maincontentinner" : $background }} tw-p-none">
<div class="tw-flex tw-flex-col tw-h-full {{ ($background == "default") ? "tw-pb-l" : "" }}">
@if(empty($fixed))
<div class="stickyHeader" style="padding:15px; height:50px; width:100%;">
<div class="grid-handler-top tw-h-[30px] tw-cursor-grab tw-float-left tw-mr-sm">
<i class="fa-solid fa-grip-vertical"></i>
</div>
@if($name != '' && $noTitle == false)
<h5 class="subtitle tw-pb-m tw-float-left tw-mr-sm" style="margin-top:-5px;">{{ __($name) }}</h5>
@endif
<div class="inlineDropDownContainer tw-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="javascript:void(0)" class="fitContent"><i class="fa-solid fa-up-right-and-down-left-from-center"></i> Resize to fit content</a></li>
@if(empty($alwaysVisible))
<li><a href="javascript:void(0)" class="removeWidget"><i class="fa fa-eye-slash"></i> Hide</a></li>
@endif
</ul>
</div>
</div>
@endif
<span class="clearall"></span>
<div class="widgetContent {{ ($background == "default") ? 'tw-px-m' : '' }}">
{{ $slot }}
</div>
</div>
<div class="clear"></div>
</div>
</div>

View File

@@ -0,0 +1,124 @@
@props([
'includeTitle' => true,
'calendar' => [],
])
@dispatchEvent('beforeCalendar')
<div class="clear minCalendar" style="position:absolute; top:10px; right:35px;">
<button class="btn btn-link btn-round-icon dropdown-toggle f-right" type="button" data-tippy-content="{{ __('text.calendar_view') }}"
data-toggle="dropdown"> <i class="fa-solid fa-calendar-week"></i></button>
<ul class="dropdown-menu pull-right">
<li>
<a class="fc-agendaDay-button fc-button fc-state-default fc-corner-right calendarViewSelect" href="javascript:void(0);"
data-value="multiMonthOneMonth"
@if($tpl->getToggleState("dashboardCalendarView") == 'multiMonthOneMonth') selected='selected' @endif>Month</a>
</li>
<li>
<a class="fc-timeGridWeek-button fc-button fc-state-default fc-corner-right calendarViewSelect" href="javascript:void(0);"
data-value="timeGridWeek" @if($tpl->getToggleState("dashboardCalendarView") == 'timeGridWeek') selected='selected' @endif>Week</a>
</li>
<li>
<a class="fc-agendaWeek-button fc-button fc-state-default calendarViewSelect" href="javascript:void(0);"
data-value="timeGridDay" @if($tpl->getToggleState("dashboardCalendarView") == 'timeGridDay' || empty($tpl->getToggleState("dashboardCalendarView")) ) selected='selected' @endif>Day</a>
</li>
<li><a class="fc-agendaWeek-button fc-button fc-state-default calendarViewSelect" href="javascript:void(0);"
data-value="listWeek" @if($tpl->getToggleState("dashboardCalendarView") == 'listWeek') selected='selected' @endif>List</a></li>
</ul>
</div>
<div class="tw-h-full minCalendar">
<div class="clear"></div>
<div class="fc-toolbar tw-z-10">
<div class="fc-left tw-flex">
<div class="day-selector tw-w-full tw-flex tw-gap-2 tw-mb-4 tw-justify-between"
@php
$currentView = $tpl->getToggleState("dashboardCalendarView") ?: 'timeGridDay';
@endphp
@if ($currentView !== 'timeGridDay') style="display:none" @endif>
@php
$today = dtHelper()->userNow();
$startOfWeek = dtHelper()->userNow()->startOf("week");
$week = [];
for($i = 0; $i < 7; $i++) {
$date = $startOfWeek->modify("+$i days");
$week[] = $date;
}
@endphp
@foreach($week as $day)
<button class="day-button tw-rounded-md tw-w-12 tw-h-12 tw-flex tw-flex-col tw-items-center tw-justify-center tw-text-sm {{ $day->format('Y-m-d') === $today->format('Y-m-d') ? 'today active' : '' }}" data-date="{{ $day->format('Y-m-d') }}">
<span class="tw-text-xs">{{ $day->format('D') }}</span>
<span class="tw-font-medium">{{ $day->format('d') }}</span>
</button>
@endforeach
</div>
</div>
<div class="clear"></div>
</div>
<div class="clear"></div>
<div class="minCalendarWrapper">
</div>
</div>
<script>
var eventSources = [];
var events = {events: [
@foreach ($calendar as $event)
{
title: {!! json_encode($event['title']) !!},
start: new Date({{ format($event['dateFrom'])->jsTimestamp() }}),
@if (isset($event['dateTo']))
end: new Date({{ format($event['dateTo'])->jsTimestamp() }}),
@endif
@if ((isset($event['allDay']) && $event['allDay'] === true))
allDay: true,
@else
allDay: false,
@endif
enitityId: {{ $event['id'] }},
@if (isset($event['eventType']) && $event['eventType'] == 'calendar')
url: '#/calendar/editEvent/{{ $event['id'] }}',
backgroundColor: '{{ $event['backgroundColor'] ?? "var(--accent2)" }}',
borderColor: '{{ $event['borderColor'] ?? "var(--accent2)" }}',
enitityType: "event",
@else
url: '#/tickets/showTicket/{{ $event['id'] }}?projectId={{ $event['projectId'] }}',
backgroundColor: '{{ $event['backgroundColor'] ?? "var(--accent2)" }}',
borderColor: '{{ $event['borderColor'] ?? "var(--accent2)" }}',
enitityType: "ticket",
@endif
},
@endforeach
]};
eventSources.push(events);
<?php
$externalCalendars = $externalCalendars ?? [];
foreach ($externalCalendars as $externalCalendar) { ?>
eventSources.push(
{
url: '<?= BASE_URL ?>/calendar/externalCal/<?= $externalCalendar['id'] ?>',
format: 'ics',
color: '<?= $externalCalendar['colorClass'] ?>',
editable: false,
}
);
<?php } ?>
var initialView = '{{ $tpl->getToggleState("dashboardCalendarView") ? $tpl->getToggleState("dashboardCalendarView") : "timeGridDay" }}';
leantime.calendarController.initWidgetCalendar(".minCalendarWrapper", initialView)
@dispatchEvent('scripts.beforeClose')
</script>

View File

@@ -0,0 +1,77 @@
@props([
'includeTitle' => true,
'allProjects' => [],
'background' => ''
])
<div id="myProjectsWidget"
hx-get="{{BASE_URL}}/widgets/myProjects/get"
hx-trigger="HTMX.updateProjectList from:body"
hx-target="#myProjectsWidget"
hx-swap="outerHTML">
@if (count($allProjects) == 0)
<br /><br />
<div class='center'>
<div style='width:70%' class='svgContainer'>
{{ __('notifications.not_assigned_to_any_project') }}
@if($login::userIsAtLeast($roles::$manager))
<br /><br />
<a href='{{ BASE_URL }}/projects/newProject' class='btn btn-primary'>{{ __('link.new_project') }}</a>
@endif
</div>
</div>
@endif
<div class="clearall"></div>
<x-global::accordion id="myProjectWidget-favorites" class="{{ $background }}">
<x-slot name="title">
My Favorites
</x-slot>
<x-slot name="content">
<div class="row">
@php
$hasFavorites = false;
@endphp
@foreach ($allProjects as $project)
@if($project['isFavorite'] == true)
<div class="col-md-4">
@include("projects::partials.projectCard", ["project" => $project, "type" => $type])
</div>
@php
$hasFavorites = true;
@endphp
@endif
@endforeach
@if($hasFavorites === false)
You don't have any favorites. 😿
@endif
</div>
</x-slot>
</x-global::accordion>
<x-global::accordion id="myProjectWidget-otherProjects" class="{{ $background }}">
<x-slot name="title">
🗂️ All Assigned Projects
</x-slot>
<x-slot name="content">
<div class="row">
@foreach ($allProjects as $project)
@if($project['isFavorite'] == false)
<div class="col-md-4">
@include("projects::partials.projectCard", ["project" => $project, "type" => $type])
</div>
@endif
@endforeach
</div>
</x-slot>
</x-global::accordion>
</div>
@dispatchEvent('afterMyProjectBox')

View File

@@ -0,0 +1,441 @@
@props([
'includeTitle' => true,
'tickets' => [],
'onTheClock' => false,
'groupBy' => '',
'allProjects' => [],
'allAssignedprojects' => [],
'projectFilter' => '',
])
@php
// Helper function to count tickets recursively
if (!function_exists('countTicketsRecursive')) {
function countTicketsRecursive($tickets) {
$count = count($tickets);
foreach ($tickets as $ticket) {
if (!empty($ticket['children'])) {
$count += countTicketsRecursive($ticket['children']);
}
}
return $count;
}
}
@endphp
<div id="yourToDoContainer"
hx-get="{{BASE_URL}}/widgets/myToDos/get"
hx-trigger="{{ \Leantime\Domain\Tickets\Htmx\HtmxTicketEvents::UPDATE }} from:body, {{ \Leantime\Domain\Tickets\Htmx\HtmxTicketEvents::SUBTASK_UPDATE }} from:body"
class="clear"
hx-swap="outerHTML"
hx-ext="json-enc"
hx-indicator="#todoWidgetLoader"
data-group-by="{{ $groupBy }}"
>
{{-- The loader lives INSIDE the swapped container: #yourToDoContainer swaps with outerHTML and
the response re-includes this partial, so a sibling loader would accumulate a duplicate
#todoWidgetLoader on every refresh. Inside the target, the swap replaces it cleanly. --}}
<div id="todoWidgetLoader" class="htmx-indicator full-width-loader">
<div class="indeterminate"></div>
</div>
<div class="clear" style="position:absolute; top:10px; right:35px;">
@dispatchEvent("beforeTodoWidgetGroupByDropdown")
<div class="btn-group left">
<button class="btn btn-link btn-round-icon dropdown-toggle f-right" type="button" data-tippy-content="{{ __('text.group_by') }}"
data-toggle="dropdown"><span class="fa-solid fa-diagram-project"></span></button>
<ul class="dropdown-menu pull-right">
<li class="nav-header">{!! __("text.group_by") !!}</li>
<li>
<span class="radio">
<input type="radio" name="groupBy"
@if($groupBy == "time") checked='checked' @endif
value="time" id="groupByDate"
hx-get="{{BASE_URL}}/widgets/myToDos/get"
hx-trigger="click"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader"
style="margin-top:4px;"
hx-vals='{"projectFilter": "{{ $projectFilter }}", "groupBy": "time" }'
/>
<label for="groupByDate">{!! __("label.dates") !!}</label>
</span>
</li>
<li>
<span class="radio">
<input type="radio"
name="groupBy"
@if($groupBy == "project") checked='checked' @endif
value="project" id="groupByProject"
hx-get="{{BASE_URL}}/widgets/myToDos/get"
hx-trigger="click"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader"
style="margin-top:4px;"
hx-vals='{"projectFilter": "{{ $projectFilter }}", "groupBy": "project" }'
/>
<label for="groupByProject">{!! __("label.project") !!}</label>
</span>
</li>
<li>
<span class="radio">
<input type="radio"
name="groupBy"
@if($groupBy == "priority") checked='checked' @endif
value="priority" id="groupByPriority"
hx-get="{{BASE_URL}}/widgets/myToDos/get"
hx-trigger="click"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader"
style="margin-top:4px;"
hx-vals='{"projectFilter": "{{ $projectFilter }}", "groupBy": "priority" }'
/>
<label for="groupByPriority">{!! __("label.priority") !!}</label>
</span>
</li>
</ul>
</div>
<div class="btn-group left ">
<button class="btn btn-link btn-round-icon dropdown-toggle f-right" type="button" data-toggle="dropdown">
<i class="fas fa-filter"></i>
@if($projectFilter != '')
<span class='badge badge-primary'>1</span>
@endif
</button>
<ul class="dropdown-menu pull-right">
<li class="nav-header">{!! __("text.filter") !!}</li>
<li
@if($projectFilter == '')
class='active'
@endif
><a href=""
hx-get="{{BASE_URL}}/widgets/myToDos/get"
hx-trigger="click"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader"
hx-vals='{"projectFilter": "all", "groupBy": "{{ $groupBy }}" }'
>{{ __('labels.all_projects') }}
</a></li>
@if($allAssignedprojects)
@foreach($allAssignedprojects as $project)
<li
@if($projectFilter == $project['id'])
class='active'
@endif
><a href=""
hx-get="{{BASE_URL}}/widgets/myToDos/get"
hx-trigger="click"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader"
hx-vals='{"projectFilter": "{{ $project['id'] }}", "groupBy": "{{ $groupBy }}" }'
>{{ $project['name'] }}</a></li>
@endforeach
@endif
</ul>
</div>
@dispatchEvent("afterTodoWidgetGroupByDropdown")
</div>
<div class="tw-flex tw-flex-col">
<div class="">
@if($tickets !== null && count($tickets) == 0)
<div class='center'>
<div style='width:30%' class='svgContainer'>
{!! file_get_contents(ROOT . "/dist/images/svg/undraw_a_moment_to_relax_bbpa.svg") !!}
</div>
<br/>
<h4>{{ __("text.no_tasks_assigned") }}</h4>
<x-global::forms.button tag="a" link="javascript:void(0);" contentRole="link" class="add-task-button" style="margin-left:0px;" data-group="emptyGroup"><i class="fa-solid fa-circle-plus"></i> {{ __('links.add_task') }}</x-global::forms.button>
<div class="quickAddForm" id="quickAddForm-emptyGroup"
style="display:none; margin-bottom:15px; padding-bottom:5px; padding-left:5px;">
<form method="post"
hx-post="{{ BASE_URL }}/widgets/myToDos/addTodo"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader">
<div class="tw-flex tw-flex-row tw-gap-2">
<div class="tw-flex-grow">
<x-global::forms.text-input variant="headline" name="headline"
style="font-size:var(--base-font-size)"
placeholder="{{ __('input.placeholders.what_are_you_working_on') }}" />
<input type="hidden" name="quickadd" value="true"/>
</div>
<div>
<select name="projectId">
@foreach($allAssignedprojects as $project)
<option value="{{ $project['id'] }}"
{{ (session('currentProject') == $project['id'] ) ? 'selected' : '' }}
>{{ $project["name"] }}</option>
@endforeach
</select>
</div>
<div>
<input type="hidden" name="milestone" value=""/>
<input type="hidden" name="status" value="3"/>
<input type="hidden" name="priority"
value=""/>
<input type="hidden" name="dateToFinish"
value="{{ date('Y-m-d', strtotime('next friday'))}}"/>
<x-global::forms.textarea name="description" class="description-input" style="display:none;"
placeholder="{{ __('input.placeholders.description') }}"></x-global::forms.textarea>
</div>
<div>
<x-global::forms.button tag="input" inputType="submit" :labelText="__('buttons.save')" name="create"
contentRole="primary"/>
<x-global::forms.button tag="a" link="javascript:void(0);" class="cancel-add-task"
data-group="emptyGroup">{{ __('buttons.cancel') }}</x-global::forms.button>
</div>
</div>
</form>
</div>
</div>
@endif
@foreach ($tickets as $groupKey => $ticketGroup)
@php
//Get first duedate if exist
$firstDueDate = null;
foreach($ticketGroup['tickets'] as $ticket) {
if($ticket['dateToFinish'] != '0000-00-00' && $ticket['dateToFinish'] != '1969-12-31 00:00:00') {
if($firstDueDate == null || $ticket['dateToFinish'] < $firstDueDate) {
$firstDueDate = $ticket['dateToFinish'];
}
}
}
@endphp
<x-global::accordion id="ticketBox1-{{ $groupKey }}-{{ $loop->index }}">
<x-slot name="title">
{!! __($ticketGroup["labelName"]) !!}
<span class="task-count" id="task-count-{{ $groupKey }}">
({{ count($ticketGroup["tickets"]) }})
</span>
</x-slot>
<x-slot name="actionlink">
<x-global::forms.button tag="a" link="javascript:void(0);" contentRole="link" class="add-task-button" style="padding:0px; padding-left:1px; width:31px; line-height:31px; height:31px; font-weight:bold; text-align: center; font-size:var(--font-size-l);" data-group="{{ $groupKey }}">
<i class="fa-solid fa-circle-plus"></i></x-global::forms.button>
</x-slot>
<x-slot name="content">
<!-- Quick Add Form for this group -->
<div class="quickAddForm" id="quickAddForm-{{ $groupKey }}"
style="display:none; margin-bottom:15px; padding-bottom:5px; padding-left:5px;">
<form method="post"
hx-post="{{ BASE_URL }}/widgets/myToDos/addTodo"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator="#todoWidgetLoader">
<div class="tw-flex tw-flex-row tw-gap-2">
<div class="tw-flex-grow">
<x-global::forms.text-input variant="headline" name="headline"
style="font-size:var(--base-font-size)"
placeholder="{{ __('input.placeholders.what_are_you_working_on') }}" />
<input type="hidden" name="quickadd" value="true"/>
</div>
<div>
<select name="projectId">
@foreach($allAssignedprojects as $project)
<option value="{{ $project['id'] }}"
{{ (($groupBy === "project" && $project['id'] == $groupKey) || ($groupBy !== "project" && session('currentProject') == $groupKey)) ? 'selected' : '' }}
>{{ $project["name"] }}</option>
@endforeach
</select>
</div>
<div>
<input type="hidden" name="milestone" value=""/>
<input type="hidden" name="status" value="3"/>
<input type="hidden" name="priority"
value="{{ $groupBy === "priority" ? $groupKey : '' }}"/>
@php
$dueDate = '';
if($groupKey === 'thisWeek'){
$dueDate = dtHelper()->userNow()->next('Friday')->formatDateForUser();
}else if($groupKey === 'overdue'){
$dueDate = dtHelper()->userNow()->subtract("3 days")->formatDateForUser();
}
@endphp
<input type="hidden" name="dateToFinish"
value="{{ $dueDate }}"/>
<x-global::forms.textarea name="description" class="description-input" style="display:none;"
placeholder="{{ __('input.placeholders.description') }}"></x-global::forms.textarea>
</div>
<div>
<x-global::forms.button tag="input" inputType="submit" :labelText="__('buttons.save')" name="create"
contentRole="primary"/>
<x-global::forms.button tag="a" link="javascript:void(0);" class="cancel-add-task"
data-group="{{ $groupKey }}">{{ __('buttons.cancel') }}</x-global::forms.button>
</div>
</div>
</form>
</div>
<div class="sortable-list" data-container-type="section" data-group-key="{{ $groupKey }}" style="padding-left:5px;">
@foreach ($ticketGroup['tickets'] as $row)
@include('widgets::partials.todoItem', ['ticket' => $row, 'statusLabels' => $statusLabels, 'onTheClock' => $onTheClock, 'tpl' => $tpl, 'level' => 0, 'groupKey' => $groupKey])
@endforeach
</div>
</x-slot>
</x-global::accordion>
@endforeach
</div>
@if(isset($hasMoreTickets) && $hasMoreTickets === true)
<!-- Global Load more trigger for infinite scroll -->
<div id="global-load-more"
class="load-more-trigger"
hx-get="{{ BASE_URL }}/widgets/myToDos/loadMore"
hx-trigger="intersect once"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-vals='{"limit": {{ $limit }}, "groupBy": "{{ $groupBy }}", "projectFilter": "{{ $projectFilter }}"}'>
<div class="tw-text-center tw-py-4">
<div class="htmx-indicator">
<div class="indeterminate"></div>
</div>
<div class="tw-text-sm tw-text-gray-500">
{{ __('text.loading_more_tasks') }}
</div>
</div>
</div>
@endif
</div>
@dispatchEvent('afterTodoListWidgetBox')
<script type="text/javascript">
@dispatchEvent('scripts.afterOpen')
jQuery(document).ready(function () {
console.debugging = true;
console.debug = function () {
if (!console.debugging) return;
console.log.apply(this, arguments);
};
var sortableEnabled = {{ $tpl->dispatchFilter('todoWidgetSortableEnabled', 'true') ? 'true' : 'false' }};
@if(session('userdata.id') != null)
leantime.ticketsController.initMilestoneDropdown();
leantime.ticketsController.initStatusDropdown();
leantime.ticketsController.initDueDateTimePickers();
if(sortableEnabled) {
// Initialize the sortable lists for hierarchical tasks
jQuery('.sortable-list').nestedSortable();
}
@else
if(sortableEnabled) {
leantime.authController.makeInputReadonly(".maincontentinner");
}
@endif
});
// Register once: this script lives inside #yourToDoContainer, which re-swaps on
// every ticket event, so an unguarded htmx.onLoad stacked a new handler per refresh.
if (!window.leantime._todoSortableOnLoadRegistered) {
window.leantime._todoSortableOnLoadRegistered = true;
htmx.onLoad(function () {
jQuery('.sortable-list').nestedSortable();
});
}
</script>
<script>
// Quick Add Task functionality
jQuery(document).ready(function () {
initAddTaskBtns();
});
// Register once (see note above — same re-swap handler-leak applies here).
if (!window.leantime._todoAddBtnsOnLoadRegistered) {
window.leantime._todoAddBtnsOnLoadRegistered = true;
htmx.onLoad(function () {
initAddTaskBtns();
});
}
function initAddTaskBtns() {
// Show the quick add form when the + button is clicked
jQuery('.add-task-button').on('click', function () {
var groupKey = jQuery(this).data('group');
jQuery('#quickAddForm-' + groupKey).show();
jQuery('#quickAddForm-' + groupKey + ' .main-title-input').focus();
});
// Hide the quick add form when cancel is clicked
jQuery('.cancel-add-task').on('click', function () {
var groupKey = jQuery(this).data('group');
jQuery('#quickAddForm-' + groupKey).hide();
jQuery('#quickAddForm-' + groupKey + ' .main-title-input').val('');
jQuery('#quickAddForm-' + groupKey + ' .description-input').val('');
});
jQuery('.ticket-title').each(function(){
let currentTitle = jQuery(this);
jQuery(this).hover(function () {
jQuery(this).find(".edit-button").show();
},
function(){
jQuery(this).find(".edit-button").hide();
});
jQuery(this).find(".edit-button").click(function() {
currentTitle.find(".edit-button").hide();
currentTitle.find('.title-text').hide();
currentTitle.find('.edit-form').show();
});
jQuery(this).find(".edit-form .cancel-edit-task").click(function() {
currentTitle.find('.title-text').show();
currentTitle.find('.edit-form').hide();
});
});
}
</script>
</div>

View File

@@ -0,0 +1,105 @@
@props([
'includeTitle' => true,
'tickets' => [],
'onTheClock' => false,
'groupBy' => '',
'allProjects' => [],
'allAssignedprojects' => [],
'projectFilter' => '',
'hasMoreTickets' => false,
'nextOffset' => 0,
'isLoadMore' => true,
])
@php
// Helper function to count tickets recursively
if (!function_exists('countTicketsRecursive')) {
function countTicketsRecursive($tickets) {
$count = count($tickets);
foreach ($tickets as $ticket) {
if (!empty($ticket['children'])) {
$count += countTicketsRecursive($ticket['children']);
}
}
return $count;
}
}
@endphp
<!-- Just append individual tasks to existing groups -->
@foreach ($tickets as $groupKey => $ticketGroup)
@if (isset($ticketGroup['tickets']) && count($ticketGroup['tickets']) > 0)
@foreach ($ticketGroup['tickets'] as $row)
<div class="additional-task" data-group-key="{{ $groupKey }}" style="display: none;">
@include('widgets::partials.todoItem', ['ticket' => $row, 'statusLabels' => $statusLabels, 'onTheClock' => $onTheClock, 'tpl' => $tpl, 'level' => 0, 'groupKey' => $groupKey])
</div>
@endforeach
@endif
@endforeach
@if(isset($hasMoreTickets) && $hasMoreTickets === true)
<!-- Global Load more trigger for infinite scroll -->
<div id="global-load-more"
class="load-more-trigger"
hx-get="{{ BASE_URL }}/widgets/myToDos/loadMore"
hx-trigger="intersect once"
hx-target="#global-load-more"
hx-swap="outerHTML"
hx-vals='{"offset": {{ $nextOffset }}, "limit": 20, "groupBy": "{{ $groupBy }}", "projectFilter": "{{ $projectFilter }}"}'>
<div class="tw-text-center tw-py-4">
<div class="htmx-indicator">
<div class="indeterminate"></div>
</div>
<div class="tw-text-sm tw-text-gray-500">
{{ __('text.loading_more_tasks') }}...
</div>
</div>
</div>
@endif
<script>
// Process additional tasks and append to existing groups
htmx.onLoad(function(content) {
const additionalTasks = content.querySelectorAll('.additional-task');
additionalTasks.forEach(function(taskDiv) {
const groupKey = taskDiv.dataset.groupKey;
const taskContent = taskDiv.innerHTML;
// Find existing group's sortable container
const existingGroup = document.querySelector(`[id*="ticketBox1-${groupKey}-"] .sortable-list`);
if (existingGroup) {
// Append task to existing group
existingGroup.insertAdjacentHTML('beforeend', taskContent);
// Update counter
const counter = document.querySelector(`#task-count-${groupKey}`);
if (counter) {
const currentText = counter.textContent;
const currentCount = parseInt(currentText.match(/\d+/)[0]) || 0;
counter.textContent = `(${currentCount + 1})`;
}
} else {
// Group doesn't exist yet - this shouldn't happen with global pagination,
// but if it does, we could create a new group here
console.warn('Group not found for key:', groupKey);
}
});
if (additionalTasks.length > 0) {
// Re-initialize nested sortable for new content
jQuery('.sortable-list').nestedSortable();
// Re-initialize interactive elements
leantime.ticketsController.initMilestoneDropdown();
leantime.ticketsController.initStatusDropdown();
leantime.ticketsController.initDueDateTimePickers();
// Re-initialize add task buttons
initAddTaskBtns();
}
});
</script>

View File

@@ -0,0 +1,17 @@
<div class="ticket-submenu">
<ul class="nav nav-pills">
<li><a href="#/tickets/{{ $ticket["id"] }}" class=""><i class="fa-solid fa-eye"></i> {{ __("links.view_todo") }}</a></li>
<li><a href="#/tickets/edit/{{ $ticket["id"] }}" class=""><i class="fa-solid fa-pencil"></i> {{ __("links.edit_todo") }}</a></li>
<li><a href="#/tickets/delete/{{ $ticket["id"] }}" class=""><i class="fa-solid fa-trash"></i> {{ __("links.delete_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']}}').slideToggle();"
class="add-subtask-link">
<i class="fa-solid fa-diagram-predecessor"></i>
Add Subtask
</a></li>
@endif
</ul>
</div>

View File

@@ -0,0 +1,311 @@
@props([
'ticket',
'statusLabels',
'onTheClock',
'tpl',
'level' => 0
])
@php
$ticketDataJson = json_encode([
'id' => $ticket['id'] ,
'title' => $ticket['headline'],
'color' => 'var(--accent2)',
'enitityType' => 'ticket',
'url' => BASE_URL.'#/tickets/showTicket/'.$ticket['id'],
], JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP);
$hasChildren = !empty($ticket['children']);
@endphp
<div class="sortable-item draggable-todo"
id="ticket_{{ $groupKey.$ticket['id'] }}"
data-item-type="{{ $ticket['type'] === 'milestone' ? 'milestone' : ($ticket['type'] === 'subtask' ? 'subtask' : 'task') }}"
data-id="{{ $ticket['id'] }}"
data-project="{{ $ticket['projectId'] }}"
data-draggable="true"
data-sort-index="{{ $ticket['sortIndex'] ?? 10 }}"
data-event='{!! $ticketDataJson !!}'>
@php
$accordionId = 'task-children-'.$groupKey.$ticket['id'];
$accordionState = $tpl->getToggleState($tpl->getToggleState("accordion_content-".$accordionId) === 'closed' ? 'closed' : 'open');
@endphp
<div
class="tw-relative ticketBox {{ $ticket['type'] === 'milestone' ? 'milestone-box priority-border- ' : 'priority-border-'.$ticket['priority'] }} {{ $hasChildren ? 'has-children' : '' }}"
data-val="{{ $ticket['id'] }}"
data-event='{!! $ticketDataJson !!}'
@if($ticket['type'] === 'milestone')
style="background: var(--secondary-background) linear-gradient(135deg, {{ $ticket['tags'] }} 0%, var(--accent1) 100%); background-repeat: no-repeat; background-size: 100% 5px; background-position: bottom;"
@endif
>
<div class="tw-absolute full-width-loader htmx-indicator-ticket-{{$ticket['id']}}">
<div class="indeterminate"></div>
</div>
@if($hasChildren)
<div id="accordion_toggle_{{$accordionId }}"
class="task-collapse-toggle accordion-toggle {{ $accordionState }}"
onclick="leantime.snippets.accordionToggle('{{ $accordionId}}');"
>
<i class="fa fa-angle-{{ $accordionState == 'closed' ? 'right' : 'down' }}"></i>
</div>
@endif
@if($ticket['type'] == 'milestone')
<div class="tw-flex tw-flex-row tw-items-center tw-gap-4">
<div class="tw-flex-grow">
<small style="display:inline-block; ">{{ $ticket['projectName'] }}</small>
<h4><a href="#/tickets/editMilestone/{{ $ticket['id'] }}"
style="font-size:var(--font-size-l);">{{ $ticket['headline'] }}</a></h4>
</div>
<div class="tw-flex-grow">
<div hx-trigger="load"
hx-indicator=".htmx-indicator-ticket-{{ $ticket['id'] }}"
hx-get="<?= BASE_URL ?>/hx/tickets/milestones/progress?milestoneId=<?= $ticket['id'] ?>&progressColor={{ trim($ticket['tags'], "#") }}">
<div class="htmx-indicator">
<?= $tpl->__('label.loading_milestone') ?>
</div>
</div>
</div>
</div>
@else
<div class="tw-flex tw-flex-row">
<div class="tw-content-center">
<div class="tw-content-center tw-mr-[10px]">
@include('tickets::partials.timerButton', ['parentTicketId' => $ticket['id'], 'onTheClock' => $onTheClock])
</div>
</div>
<div class="tw-flex-1 ticket-title ticket-title-wrapper">
<div class="title-text">
<small style="display:inline-block; ">{{ $ticket['projectName'] }}</small> <br/>
<strong><a href="#/tickets/showTicket/{{ $ticket['id'] }}" preload="mouseover"
class="ticket-headline-{{ $ticket['id'] }}">{{ $ticket['headline'] }}</a></strong>
&nbsp;<a href="javascript:void(0);" class="tw-hidden edit-button"
data-tippy-content="{{ __('text.edit_task_headline') }}"><i class="fa fa-edit"></i></a>
</div>
<div class="tw-hidden edit-form">
<form class="tw-flex tw-flex-row tw-items-center tw-gap-2"
hx-post="{{ BASE_URL }}/hx/widgets/myToDos/updateTitle"
hx-target=".ticket-headline-{{ $ticket['id'] }}"
onsubmit="jQuery(this).closest('.edit-form').find('.cancel-edit-task').click();"
>
<input type="hidden" name="id" value="{{ $ticket['id'] }}"/>
<div>
<x-global::forms.text-input variant="headline"
style="font-size:var(--base-font-size); margin-bottom:0px"
value="{{ $ticket['headline'] }}" name="headline" />
</div>
<div>
<x-global::forms.button inputType="submit" name="edit" contentRole="primary">
<i class="fa fa-check"></i>
</x-global::forms.button>
</div>
<div>
<x-global::forms.button tag="a" link="javascript:void(0);" class="cancel-edit-task" data-group="{{ $groupKey }}"><i
class="fa fa-x"></i></x-global::forms.button>
</div>
</form>
</div>
</div>
@dispatchEvent('beforePlaceholder', ['ticket' => (object)$ticket])
<div class="placeholder-container tw-flex-1 tw-flex tw-flex-row tw-content-center">
@dispatchEvent('placeholderContainer', ['ticket' => (object)$ticket])
</div>
@dispatchEvent('beforeDueDate', ['ticket' => (object)$ticket])
<div
class="due-date-container tw-flex-1 tw-justify-right tw-flex tw-flex-row tw-justify-end tw-content-center due-date-wrapper">
<div class="tw-content-center">
<div class="date-picker-form-control">
<i class="fa-solid fa-business-time infoIcon"
data-tippy-content="{{ __("label.due") }}"></i>
<input id="due-date-picker-{{ $ticket['id'] }}"
type="text"
title="{{ __("label.due") }}"
value="{{ format($ticket['dateToFinish'])->date(__("text.anytime")) }}"
class="duedates secretInput"
style="margin-left:0px; width:100px;"
data-id="{{ $ticket['id'] }}"
onchange="jQuery('#due-date-picker-trigger-{{ $ticket['id'] }}').text(this.value);"
name="date"
hx-post="{{ BASE_URL }}/widgets/myToDos/updateDueDate"
hx-trigger="change"
hx-vals='{"id": "{{ $ticket['id'] }}"}'
hx-indicator=".htmx-indicator-ticket-{{ $ticket['id'] }}"/>
<button class="reset-button"
data-id="{{ $ticket['id'] }}"
id="reset-date-{{ $ticket['id'] }}"
hx-post="{{ BASE_URL }}/widgets/myToDos/updateDueDate"
hx-vals='{"id": "{{ $ticket['id'] }}", "date": ""}'
hx-indicator=".htmx-indicator-ticket-{{ $ticket['id'] }}">
<span class="sr-only">{{ __("language.resetDate") }}</span>
<i class="fa fa-close"></i>
</button>
</div>
</div>
<div class="tw-content-center">
@dispatchEvent('afterDueDate', ['ticket' => (object)$ticket])
</div>
</div>
@dispatchEvent('beforeStatusUpdate')
<div
class="status-container tw-flex-1 tw-justify-items-end tw-flex tw-flex-row tw-justify-end tw-gap-2 tw-content-center">
<div class="tw-content-center tw-mr-[10px] dropdown ticketDropdown statusDropdown colorized show">
<a class="dropdown-toggle f-left status {{ $statusLabels[$ticket['projectId']][$ticket['status']]["class"] ?? 'label-default' }}"
href="javascript:void(0);"
role="button"
id="statusDropdownMenuLink{{ $ticket['id'] }}"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<span class="text">
@if(isset($statusLabels[$ticket['projectId']][$ticket['status']]))
{{ $statusLabels[$ticket['projectId']][$ticket['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{{ $ticket['id'] }}">
<li class="nav-header border">{{ __("dropdown.choose_status") }}</li>
@foreach ($statusLabels[$ticket['projectId']] as $key => $label)
<li class='dropdown-item'>
<a href='javascript:void(0);'
class='{{ $label["class"] }}'
data-label='{{ $label["name"] }}'
data-value='{{ $ticket['id'] }}_{{ $key }}_{{ $label["class"] }}'
id='ticketStatusChange{{$ticket['id'] . $key }}'
hx-post="{{ BASE_URL }}/widgets/myToDos/updateStatus"
hx-swap="none"
hx-vals='{"id": "{{ $ticket['id'] }}", "status": "{{ $key }}"}'>
{{ $label["name"] }}
</a>
</li>
@endforeach
</ul>
</div>
<div class="tw-content-center">
<div class="scheduler">
@if( $ticket['editFrom'] != "0000-00-00 00:00:00" && $ticket['editFrom'] != "1969-12-31 00:00:00")
<i class="fa-solid fa-calendar-check infoIcon" style="color:var(--accent2)"
data-tippy-content="{{ __('text.schedule_to_start_on') }} {{ format($ticket['editFrom'])->date() }}"></i>
@else
<i class="fa-regular fa-calendar-xmark infoIcon"
data-tippy-content="{{ __('text.not_scheduled_drag_ai') }}"></i>
@endif
</div>
</div>
<div class="tw-content-center">
@include("tickets::partials.ticketsubmenu", ["ticket" => $ticket, "onTheClock" => $onTheClock, "allowSubtaskCreation" => true])
</div>
</div>
</div>
@endif
</div>
<!-- Subtask Form -->
<div id="subtask-form-{{$ticket['id']}}" class="subtask-form ticketBox"
style="display:none; margin:10px; margin-left:40px;">
<form class="form-group"
hx-post="{{ BASE_URL }}/widgets/myToDos/addSubtask?ticketId={{$ticket['id']}}"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator=".htmx-indicator-ticket-{{ $ticket['id'] }}">
<input type="hidden" value="new" name="subtaskId"/>
<input type="hidden" value="1" name="subtaskSave"/>
<input type="hidden" value="{{ format($ticket['dateToFinish'])->date() }}" name="dateToFinish"/>
<div class="tw-flex tw-flex-row tw-gap-2">
<div class="tw-flex-grow">
<x-global::forms.text-input name="headline" variant="headline"
style="font-size:var(--base-font-size)"
placeholder="{{ __('input.placeholders.what_are_you_working_on') }}" />
</div>
<div>
<input type="hidden" name="status" value="3"/>
<x-global::forms.button inputType="submit" contentRole="primary">{{ __('buttons.save') }}</x-global::forms.button>
<x-global::forms.button tag="a" link="javascript:void(0);"
onclick="jQuery('#subtask-form-{{$ticket['id']}}').toggle();">{{ __('buttons.cancel') }}</x-global::forms.button>
</div>
</div>
</form>
</div>
<!-- End Subtask Form -->
<div id="accordion_content-{{ $accordionId }}"
style="{{ $accordionState =='closed' ? 'display:none;' : '' }}"
class="sortable-list task-children {{ $tpl->getToggleState("user.".session('userdata.id').".taskCollapsed.".$ticket['id'], 'open') }}"
data-container-type="{{ $ticket['type'] == 'milestone' ? 'milestone' : ($ticket['type'] == 'subtask' ? 'subtask' : 'task') }}">
@foreach(($ticket['children'] ?? []) as $childTicket)
@include('widgets::partials.todoItem', ['ticket' => $childTicket, 'statusLabels' => $statusLabels, 'onTheClock' => $onTheClock, 'tpl' => $tpl, 'level' => $level + 1, 'groupKey' => $groupKey])
@endforeach
@if($level == 0 && $ticket['type'] === "milestone")
<!-- Subtask Form -->
<div id="task-add-form-{{ $groupKey }}-{{$ticket['id']}}" class="subtask-form ticketBox"
style="display:none; margin:5px 0px;">
<form class="form-group"
id="task-add-form-{{ $groupKey }}-{{$ticket['id']}}-form"
hx-post="{{ BASE_URL }}/widgets/myToDos/addTodo"
hx-target="#yourToDoContainer"
hx-swap="outerHTML"
hx-indicator=".htmx-indicator-ticket-{{ $ticket['id'] }}"
onsubmit="jQuery(this).find('.main-title-input').attr('readonly', true);"
>
<input type="hidden" name="milestone"
value="{{ $ticket['type'] == "milestone" ? $ticket['id'] : '' }}"/>
<input type="hidden" name="status" value="3"/>
<input type="hidden" name="quickadd" value="true"/>
<input type="hidden" name="sortIndex"
value="{{ isset($ticket['children']) ? ((collect($ticket['children'])->last()['sortIndex'] ?? 10)+5) : 10 }}"/>
<input type="hidden" name="projectId" value="{{ $ticket['projectId'] }}"/>
<input type="hidden" name="priority"
value="{{ $groupBy === "priority" ? $groupKey : '' }}"/>
<input type="hidden" name="dateToFinish"
@if($groupKey === 'thisWeek')
value="{{ dtHelper()->userNow()->next('Friday')->formatDateForUser() }}"
@elseif($groupKey === 'overdue')
value="{{ dtHelper()->userNow()->yesterday()->formatDateForUser() }}"
@else
value=""
@endif
/>
<div class="tw-flex tw-flex-row tw-gap-2">
<div class="tw-flex-grow">
<x-global::forms.text-input name="headline" variant="headline"
style="font-size:var(--base-font-size)"
placeholder="{{ __('input.placeholders.what_are_you_working_on') }}" />
</div>
<div>
<input type="hidden" name="status" value="3"/>
<x-global::forms.button inputType="submit" contentRole="primary">{{ __('buttons.save') }}</x-global::forms.button>
<x-global::forms.button tag="a" link="javascript:void(0);"
onclick="jQuery('#task-add-form-{{ $groupKey }}-{{$ticket['id']}}').toggle(); jQuery('#task-add-form-{{ $groupKey }}-{{$ticket['id']}}-handler').toggle();">{{ __('buttons.cancel') }}</x-global::forms.button>
</div>
</div>
</form>
</div>
<!-- End Subtask Form -->
<a href="javascript:void(0);" id="task-add-form-{{ $groupKey }}-{{$ticket['id']}}-handler"
onclick="jQuery(this).toggle(); jQuery('#task-add-form-{{ $groupKey }}-{{$ticket['id']}}').toggle(); "><i
class="fa fa-plus-circle"></i> {{ __('links.add_task') }}</a>
@endif
</div>
</div>

View File

@@ -0,0 +1,75 @@
@props([
'includeTitle' => true,
'randomImage' => '',
'totalTickets' => 0,
'projectCount' => 0,
'closedTicketsCount' => 0,
'ticketsInGoals' => 0,
'doneTodayCount' => 0,
'totalTodayCount' => 0,
])
<div class="welcome-widget">
<div style="padding:0px 0px">
<div style="font-size:18px; color:var(--main-titles-color); padding-bottom:15px; padding-top:8px">
👋 {{ __('text.hi') }} {{ session()->get("userdata.name") }}
<div class="tw-float-right">
<x-global::forms.button tag="a" link="{{ BASE_URL }}/users/editOwn#theme" contentRole="link" style="color:var(--main-titles-color); padding:0px; width:31px; line-height:31px; text-align: center;" data-tippy-content="{{ __('text.update_theme') }}">
<i class="fa-solid fa-palette"></i>
</x-global::forms.button>
<x-global::forms.button tag="a" link="#/widgets/widgetManager" contentRole="link" style="color:var(--main-titles-color); padding:0px; width:31px; line-height:31px; text-align: center;" data-tippy-content="{{ __('text.update_dashboard') }}">
<span class="fa fa-fw fa-cogs"></span>
@if($showSettingsIndicator)
<span class='new-indicator'></span>
@endif
</x-global::forms.button>
</div>
</div>
<div class="tw-flex tw-gap-x-[10px]">
<div class="bigNumberBox tw-flex-1 tw-flex-grow">
<div class="bigNumberBoxInner">
<div class="bigNumberBoxNumber">⏱️ {{ $doneTodayCount }}/{{ $totalTodayCount }} </div>
<div class="bigNumberBoxText">{{ __("welcome_widget.timeboxed_completed") }}</div>
</div>
</div>
<div class="bigNumberBox tw-flex-1 tw-flex-grow">
<div class="bigNumberBoxInner">
<div class="bigNumberBoxNumber">🥳 {{ $closedTicketsCount }} </div>
<div class="bigNumberBoxText">{{ __("welcome_widget.tasks_completed") }}</div>
</div>
</div>
<div class="bigNumberBox tw-flex-1 tw-flex-grow ">
<div class="bigNumberBoxInner">
<div class="bigNumberBoxNumber">📥 {{ $totalTickets }} </div>
<div class="bigNumberBoxText">{{ __("welcome_widget.tasks_left") }}</div>
</div>
</div>
<div class="bigNumberBox tw-flex-1 tw-flex-grow">
<div class="bigNumberBoxInner">
<div class="bigNumberBoxNumber">🎯 {{ $ticketsInGoals }} </div>
<div class="bigNumberBoxText">{{ __("welcome_widget.goals_contributing_to") }}</div>
</div>
</div>
</div>
</div>
<div class="clear"></div>
@dispatchEvent('afterWelcomeMessage')
<div class="clear"></div>
</div>
@dispatchEvent('afterWelcomeMessageBox')

View File

@@ -0,0 +1,39 @@
<div class="" style="min-width:50%;">
<h1>{{ __("headlines.widget_manager") }}</h1>
<x-global::forms.button tag="a" contentRole="secondary" class="pull-right" link="{{ BASE_URL }}/dashboard/home?resetDashboard=true" style="margin-bottom:10px;"><i class="fa-solid fa-arrow-rotate-left"></i> Reset Dashboard</x-global::forms.button>
<p>{{ __("text.choose_widgets") }}</p>
<br />
<div class="row">
@foreach($availableWidgets as $widgetId => $widget)
@if($widget->alwaysVisible !== true)
@php( $widget->name = __($widget->name))
@php( $widget->description = __($widget->description))
<div class="col-md-4">
<div class="projectBox tw-p-m tw-min-w-[250px] @if(in_array($widgetId, array_keys($newWidgets)) && !isset($activeWidgets[$widgetId])) newWidget @endif">
<h5>{{ $widget->name }}</h5>
<p>{!! $widget->description !!} </p>
<div class="right">
@if($widget->alwaysVisible == false)
<input
type="checkbox"
class="toggle"
id="widget-toggle-{{ $widget->id }}"
onclick="leantime.widgetController.toggleWidgetVisibility('{{ $widget->id }}', this, {{ json_encode($widget) }})"
@if(isset($activeWidgets[$widget->id]))
checked='checked'
@if(isset($activeWidgets[$widget->id]->isNew) && $activeWidgets[$widget->id]->isNew)
data-is-new="true"
@endif
@endif
/>
<label for="widget-toggle-{{ $widget->id }}"></label>
@endif
</div>
<div class="clearall"></div>
</div>
</div>
@endif
@endforeach
</div>
<div class="clear"></div>
</div>