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,47 @@
<h4 class="widgettitle title-light">
<i class="fa-solid fa-mountain"></i>
{{ empty($bigRock['title']) ? __('label.create_new_goalboard') : __('label.goalboard') }} {{ $bigRock['title'] }}
</h4>
<form class="formModal" method="post"
action="{{ BASE_URL }}/goalcanvas/bigRock/{{ !empty($bigRock['id']) ? $bigRock['id'] : '' }}">
<br />
<label>{{ __('label.goal_description') }}</label>
<x-global::forms.text-input name="title" id="wikiTitle" value="{{ $bigRock['title'] }}" style="width:100%;" /><br />
<br />
<div class="row">
<div class="col-md-6">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" id="saveBtn" />
</div>
<div class="col-md-6 align-right padding-top-sm">
</div>
</div>
</form>
<script>
jQuery(document).ready(function() {
@if (isset($_GET['closeModal']))
jQuery.nmTop().close();
@endif
if (jQuery("#wikiTitle").val().length >= 2) {
jQuery("#saveBtn").removeAttr("disabled");
} else {
jQuery("#saveBtn").attr("disabled", "disabled");
}
jQuery("#wikiTitle").keypress(function() {
if (jQuery("#wikiTitle").val().length >= 2) {
jQuery("#saveBtn").removeAttr("disabled");
} else {
jQuery("#saveBtn").attr("disabled", "disabled");
}
})
});
</script>

View File

@@ -0,0 +1,65 @@
@extends($layout)
@section('content')
@php
/**
* canvasComment.inc template - Generic template for comments
*
*/
@endphp
<script type="text/javascript">
window.onload = function() {
if (!window.jQuery) {
//It's not a modal
location.href = "{{ BASE_URL }}/goalcanvas/showCanvas?showModal={{ $canvasItem['id'] }}";
}
}
</script>
<div class="showDialogOnLoad" style="display:none;">
<h4 class="widgettitle title-light" style="padding-bottom: 0">
<i class="fas {{ $canvasTypes[$canvasItem['box']]['icon'] }}"></i>
{{ $canvasTypes[$canvasItem['box']]['title'] }}
</h4>
<hr style="margin-top: 5px; margin-bottom: 15px;">
<h5 style="padding-left: 40px"><strong>{{ $canvasItem['description'] }}</strong></h5>
@if ($id !== '')
<br />
<input type="hidden" name="comment" value="1" />
<h4 class="widgettitle title-light">
<span class="fa fa-comments"></span>{{ __('subtitles.discussion') }}
</h4>
@include('comments::submodules.generalComment', ['formUrl' => '/goalcanvas/editCanvasComment/' . $id])
@endif
</div>
<script type="text/javascript">
jQuery(document).ready(function() {
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initSimpleEditor();
}
@if(!$login::userIsAtLeast($roles::$editor))
leantime.authController.makeInputReadonly(".nyroModalCont");
@endif;
@if($login::userHasRole([$roles::$commenter]))
leantime.commentsController.enableCommenterForms();
@endif;
})
</script>
@endsection

View File

@@ -0,0 +1,471 @@
@extends($layout)
@section('content')
@php
$hiddenRelatesLabels = $relatesLabels ?? [];
// Metric preview — from the SAVED values; re-renders on save.
$mType = $canvasItem['metricType'] ?: 'number'; // new goals default to # (no math)
$mStart = (float) ($canvasItem['startValue'] ?? 0);
$mCur = (float) ($canvasItem['currentValue'] ?? 0);
$mGoal = (float) ($canvasItem['endValue'] ?? 0);
$mPct = ($mGoal - $mStart) != 0 ? max(0, min(100, (($mCur - $mStart) / ($mGoal - $mStart)) * 100)) : 0;
$fmtM = function ($v) use ($mType) {
$v = (float) $v;
if ($mType === 'percent') return rtrim(rtrim(number_format($v, 2), '0'), '.') . '%';
if ($mType === 'currency') return '$' . number_format($v, $v == floor($v) ? 0 : 2);
return rtrim(rtrim(number_format($v, 2), '0'), '.');
};
@endphp
<script type="text/javascript">
window.onload = function() {
if (!window.jQuery) {
location.href = "{{ BASE_URL }}/goalcanvas/showCanvas?showModal={{ $canvasItem['id'] }}";
}
}
</script>
<style>
/* ── Goal dialog v1 — tabbed (Goal / Progress / Milestones) so only one
zone shows at a time. Presentation only: fields keep name/id/class. ── */
/* System design tokens, not a private palette (alignment pass
2026-08-03): ink/lines/accent/sizes derive from the theme, so the
dialog matches the rest of the app in BOTH themes. */
.gvDialog{width:860px;max-width:min(860px, 94vw);padding:6px 18px 14px;
--gv-acc:var(--accent1);
--gv-acc2:var(--accent1);
--gv-line:var(--main-border-color);
--gv-line-soft:color-mix(in srgb, var(--main-border-color) 55%, transparent);
--gv-ink:var(--primary-font-color);
--gv-ink2:color-mix(in srgb, var(--primary-font-color) 68%, transparent);
--gv-soft:var(--secondary-background);}
/* Two-column split (Docs-article pattern, review 2026-08-03): main
work area left, "Details" rail right with the essentials. The rail
hugs its content (no full-height stretch — that stranded Delete in
dead space) and keeps a tight label-over-field rhythm. */
.gv-cols{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:0 28px;align-items:start;}
.gv-side{border-left:1px solid var(--gv-line-soft);padding-left:24px;display:flex;flex-direction:column;gap:16px;}
.gv-side-head{margin:0;}
.gv-side label{margin-bottom:5px;}
.gv-side .gv-dates{grid-template-columns:1fr;max-width:none;gap:12px;}
.gv-side .gv-delete-slot{padding-top:16px;border-top:1px solid var(--gv-line-soft);}
/* Discussion sits under the MAIN column (its own form — kept outside
the goal form so the nested-form parse never orphans the Save
buttons again). */
.gv-discussion{margin-right:calc(240px + 28px);}
/* Discussion stays quiet — smaller avatar so the profile color doesn't
compete with the work area (review 2026-08-03). */
.gv-discussion .commentImage img,.gv-discussion .commentImage .profileImage{width:30px!important;height:30px!important;}
@media (max-width:900px){.gv-cols{grid-template-columns:1fr;}.gv-side{border-left:none;padding-left:0;}.gv-discussion{margin-right:0;}}
/* Section headers ride the SYSTEM recipe (h4.widgettitle.title-light,
same as the task modal) — no dialog-private eyebrow styles. */
.gvDialog > h4.widgettitle{margin:2px 0 12px;}
.gvDialog label{font-size:var(--font-size-s);font-weight:600;color:var(--gv-ink2);display:block;margin:0 0 6px;}
.gv-field-lbl{font-size:var(--font-size-s);color:var(--gv-ink2);margin:0 0 6px;}
.gv-unit{font-size:var(--font-size-xs);font-weight:700;color:var(--gv-acc);opacity:.85;}
/* tab bar — report deck style (gradient bar + translucent group + white active pill) */
/* Tab visuals come from the shared floating-pill standard
(tab-group.css: .lt-tabs--floating + --onlight for this white
modal surface); only the dialog-specific spacing stays here. */
.gv-tabs{margin:0 0 16px;}
.gv-tab i,.gv-tab span[class*="fa"]{font-size:12px;}
.gv-panel{min-height:170px;}
.gv-row{margin-bottom:18px;}
/* inputs + selects */
.gvDialog input[name="title"]{font-size:var(--font-size-xxl)!important;font-weight:600!important;line-height:1.25!important;color:var(--gv-ink)!important;border:none!important;border-bottom:1px solid var(--gv-line)!important;border-radius:0!important;padding:4px 2px 9px!important;background:transparent!important;box-shadow:none!important;height:auto!important;width:100%!important;}
.gvDialog input[name="title"]:focus{border-bottom-color:var(--gv-acc)!important;outline:none!important;box-shadow:none!important;}
.gvDialog input[name="title"]::placeholder{color:var(--gv-ink2);font-weight:500;}
.gvDialog input[type="number"]:not(.gv-mb-input),.gvDialog input[type="text"]:not([name="title"]),.gvDialog select[name="metricType"],.gvDialog input.startDate,.gvDialog input.endDate{border:1px solid var(--gv-line)!important;border-radius:var(--input-radius, 9px)!important;padding:9px 11px!important;font-size:var(--base-font-size)!important;color:var(--gv-ink)!important;background:var(--input-background, #fff)!important;box-shadow:none!important;height:auto!important;width:100%!important;}
.gvDialog input:focus:not([name="title"]):not(.gv-mb-input),.gvDialog select:focus{border-color:var(--gv-acc)!important;outline:none!important;box-shadow:0 0 0 3px rgba(0,100,122,.09)!important;}
/* Progress readout — deliberately QUIET (review 2026-08-03: the big
teal number + gradient card pulled the eye away from the actual
task). Ink-colored number, thin flat bar, no card, no gradient —
the page's color budget belongs to the action (Save / the input). */
.gv-metric-bar{padding:2px 2px 0;margin:4px 0 0;}
.gv-mb-metric{font-size:var(--font-size-s);color:var(--gv-ink2);margin-bottom:7px;}
.gv-metric-bar .gv-mb-top{display:flex;align-items:baseline;gap:6px;margin-bottom:8px;}
.gv-mb-togo{margin-left:auto;font-size:var(--font-size-s);color:var(--gv-ink2);}
.gv-mb-barrow{display:grid;grid-template-columns:minmax(0,1fr) 38px;gap:12px;align-items:start;}
.gv-mb-pct{font-size:var(--font-size-s);color:var(--gv-ink2);text-align:right;font-variant-numeric:tabular-nums;align-self:start;line-height:12px;}
/* Scored track — the RA planbar recipe: striped remaining segment,
quarter score marks, and a position marker with a halo. */
.gv-track--scored{position:relative;height:12px;border-radius:6px;overflow:visible;
background:repeating-linear-gradient(135deg, var(--gv-line-soft) 0 5px, color-mix(in srgb, var(--main-border-color) 30%, transparent) 5px 10px);}
.gv-track--scored .gv-fill{position:absolute;inset:0 auto 0 0;border-radius:6px 0 0 6px;opacity:1;}
.gv-tick{position:absolute;top:2px;bottom:2px;width:1px;background:color-mix(in srgb, var(--gv-ink) 22%, transparent);}
.gv-marker{position:absolute;top:-3px;bottom:-3px;width:2px;border-radius:2px;background:var(--gv-ink);box-shadow:0 0 0 1.5px var(--primary-background, #fff);transform:translateX(-1px);}
.gv-scale{display:flex;justify-content:space-between;margin-top:5px;font-size:var(--font-size-xs);color:var(--gv-ink2);font-variant-numeric:tabular-nums;}
/* The input hugs its digits (no fill-in-the-blank dashes past the
number) in browsers with field-sizing; others keep the fixed ch. */
@supports (field-sizing: content){
.gvDialog .gv-metric-bar input.gv-mb-input{field-sizing:content;width:auto!important;min-width:2.5ch!important;max-width:10ch!important;}
}
.gv-metric-bar .gv-mb-now{font-size:var(--font-size-xl);font-weight:700;letter-spacing:-.2px;color:var(--gv-ink);line-height:1;}
.gv-metric-bar .gv-mb-of{font-size:var(--font-size-s);color:var(--gv-ink2);}
.gv-metric-bar .gv-mb-of b{color:var(--gv-ink);font-weight:600;}
.gv-track{height:6px;background:var(--gv-line-soft);border-radius:4px;overflow:hidden;}
.gv-fill{height:100%;border-radius:4px;background:var(--gv-acc);opacity:.8;}
.gv-values{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;max-width:480px;}
/* Inline value edit — the RA pattern: the number IS the input.
(Selector carries the element+class so the dialog's generic
input[type=number] rule — same importance — can never outrank it.) */
.gvDialog .gv-metric-bar input.gv-mb-input{font-size:var(--font-size-xl)!important;font-weight:700!important;color:var(--gv-ink)!important;border:none!important;border-bottom:1px dashed var(--gv-line)!important;border-radius:0!important;background:transparent!important;padding:0 2px 2px!important;width:5.5ch!important;height:auto!important;box-shadow:none!important;-moz-appearance:textfield;}
.gv-mb-input::-webkit-outer-spin-button,.gv-mb-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0;}
.gvDialog .gv-metric-bar input.gv-mb-input:hover{border-bottom-color:var(--gv-ink2)!important;}
.gvDialog .gv-metric-bar input.gv-mb-input:focus{border-bottom:1px solid var(--gv-acc)!important;outline:none!important;box-shadow:none!important;}
/* Milestone bars on the Progress tab — read-only rows, quiet. */
.gv-ms-bars{margin-top:22px;padding-top:16px;border-top:1px solid var(--gv-line-soft);display:flex;flex-direction:column;gap:11px;}
.gv-msb-row{display:grid;grid-template-columns:9px minmax(0,1fr) 130px 38px;align-items:center;gap:12px;}
.gv-msb-dot{width:9px;height:9px;border-radius:50%;}
.gv-msb-name{font-size:var(--font-size-s);color:var(--gv-ink2);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.gv-msb-name:hover{color:var(--gv-acc);text-decoration:underline;text-underline-offset:3px;}
.gv-msb-track{height:5px;background:var(--gv-line-soft);border-radius:4px;overflow:hidden;}
.gv-msb-fill{height:100%;border-radius:4px;opacity:.75;}
.gv-msb-pct{font-size:var(--font-size-s);color:var(--gv-ink2);text-align:right;font-variant-numeric:tabular-nums;}
/* Milestones tab — MANAGEMENT list (RA line-item style). */
.gv-ms-list{display:flex;flex-direction:column;}
.gv-ms-item{display:flex;align-items:center;gap:10px;padding:9px 2px;border-bottom:1px solid var(--gv-line-soft);}
.gv-ms-name{font-size:var(--base-font-size);color:var(--gv-ink);text-decoration:none;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.gv-ms-name:hover{color:var(--gv-acc);text-decoration:underline;text-underline-offset:3px;}
.gv-ms-due{font-size:var(--font-size-s);color:var(--gv-ink2);flex:none;}
.gv-ms-remove{flex:none;background:transparent;border:none;cursor:pointer;padding:6px 8px;opacity:.55;color:var(--gv-ink2);}
.gv-ms-remove:hover{opacity:1;color:var(--gv-acc);}
.gv-values .gv-field-lbl{font-size:11px;}
.gv-dates{display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:360px;}
/* milestones panel */
.gv-ms-head{display:flex;align-items:center;gap:10px;margin-bottom:12px;}
.gv-ms-summary{font-size:var(--font-size-s);color:var(--gv-ink2);}
.gv-ms-summary b{color:var(--gv-ink);font-weight:700;}
.gv-ms-actions{margin-left:auto;display:flex;align-items:center;gap:14px;}
.gv-ms-act{background:none!important;border:none!important;cursor:pointer;color:var(--gv-ink2)!important;font-size:15px;padding:0;line-height:1;opacity:.65;transition:opacity .12s,color .12s;}
.gv-ms-act:hover{opacity:1;color:var(--gv-acc)!important;}
.gv-ms-actions .helperTooltip{color:var(--gv-ink2)!important;opacity:.55;}
/* discussion sub-heading inside the Goal tab */
/* actions (always visible under the tabs) */
.gv-actions{display:flex;align-items:center;gap:10px;margin-top:22px;padding-top:16px;border-top:1px solid var(--gv-line);}
.gv-actions .gv-delete{margin-left:auto;}
@media (max-width:560px){.gv-values{grid-template-columns:1fr 1fr;}}
</style>
<div class="gvDialog">
{{-- Section headers use the SYSTEM modal recipe (h4.widgettitle
.title-light same as Subtasks/Discussion/Schedule on the task
modal), not a dialog-private eyebrow style. --}}
<h4 class="widgettitle title-light"><i class="fas {{ $canvasTypes[$canvasItem['box']]['icon'] }}"></i> {{ $canvasTypes[$canvasItem['box']]['title'] }}</h4>
<form class="formModal" method="post" action="{{ BASE_URL . "/goalcanvas/editCanvasItem/$id" }}">
<input type="hidden" value="{{ $currentCanvas }}" name="canvasId">
<input type="hidden" value="{{ $canvasItem['box'] }}" name="box" id="box">
<input type="hidden" value="{{ $id }}" name="itemId" id="itemId">
<input type="hidden" name="changeItem" value="1">
<div class="gv-cols">
<div class="gv-main">
{{-- ── Tabs on top the nav frames the whole workspace (review
2026-08-03). The old Goal tab dissolved: name lives below the
tabs (shared by both), status/dates/relates in the Details
rail. New goals have only Progress, so no bar at all. ── --}}
@if ($id !== '')
<div class="gv-tabs lt-tabs lt-tabs--floating lt-tabs--onlight" role="tablist" aria-label="{{ __('goalcanvas.tabs_label') }}">
<div class="gv-tab-group lt-tabs-group">
<button type="button" class="gv-tab lt-tab" role="tab" id="gvTab-edit" aria-controls="gvPanel-edit" aria-selected="false" data-tab="edit"><i class="fa-solid fa-pen" aria-hidden="true"></i> {{ __('links.edit') }}</button>
<button type="button" class="gv-tab lt-tab" role="tab" id="gvTab-progress" aria-controls="gvPanel-progress" aria-selected="false" data-tab="progress"><i class="fa-solid fa-ranking-star" aria-hidden="true"></i> {{ __('goalcanvas.tab_progress') }}</button>
<button type="button" class="gv-tab lt-tab" role="tab" id="gvTab-milestones" aria-controls="gvPanel-milestones" aria-selected="false" data-tab="milestones"><span class="fa fa-flag-checkered" aria-hidden="true"></span> {{ __("headlines.milestones") }}</button>
</div>
</div>
@endif
{{-- Name one label only (the placeholder); the modal's GOAL
header already names the object, so no third repetition. --}}
<div class="gv-row">
<x-global::forms.text-input name="title" id="goalTitleInput" value="{{ $canvasItem['title'] }}" placeholder="{{ __('goalcanvas.name_goal') }}" aria-label="{{ __('goalcanvas.name_goal') }}" style="width:100%" />
</div>
{{-- ── Tab: Edit — the goal's DEFINITION (metric, type, start,
target). One-time setup, separated from the recurring
monitoring job (review 2026-08-03). --}}
<div class="gv-panel" data-panel="edit" role="tabpanel" id="gvPanel-edit" aria-labelledby="gvTab-edit" tabindex="0">
<div id="measureGoalContainer" class="gv-row">
<label class="gv-field-lbl" for="goalDescriptionInput">{{ __('goalcanvas.metric_label') }}</label>
<x-global::forms.text-input name="description" id="goalDescriptionInput" value="{{ $canvasItem['description'] }}" style="width:100%" />
</div>
<div class="gv-values">
<div>
<label class="gv-field-lbl" for="goalMetricType">{{ __('label.type') }}</label>
<select name="metricType" id="goalMetricType">
<option value="number" @if ($mType == 'number') selected @endif>{{ __('goalcanvas.type_number') }}</option>
<option value="percent" @if ($mType == 'percent') selected @endif>{{ __('goalcanvas.type_percent') }}</option>
<option value="currency" @if ($mType == 'currency') selected @endif>{{ __('goalcanvas.type_currency') }}</option>
</select>
</div>
<div>
<label class="gv-field-lbl" for="goalStartValue">{{ __('goalcanvas.v_start') }} <span class="gv-unit"></span></label>
<x-global::forms.text-input type="number" step="0.01" name="startValue" id="goalStartValue" value="{{ $canvasItem['startValue'] }}" style="width:100%" />
</div>
<div>
<label class="gv-field-lbl" for="goalEndValue">{{ __('goalcanvas.v_goal') }} <span class="gv-unit"></span></label>
<x-global::forms.text-input type="number" step="0.01" name="endValue" id="goalEndValue" value="{{ $canvasItem['endValue'] }}" style="width:100%" />
</div>
</div>
</div>
{{-- ── Tab: Progress MONITORING. The RA pattern: the readout's
number IS the input (click in, type, blur = saved via HTMX).
All the bars live here — the goal metric plus each linked
milestone's own read-only bar. The Milestones tab MANAGES the
links; this tab watches them. --}}
<div class="gv-panel" data-panel="progress" role="tabpanel" id="gvPanel-progress" aria-labelledby="gvTab-progress" tabindex="0">
@include('goalcanvas::partials.progressReadout')
{{-- Milestone bars read-only context, one quiet row per
linked milestone. Goal progress stays metric-defined
(Marcel): these never aggregate into the bar above. --}}
@if (count($goalMilestones ?? []) > 0)
<div class="gv-ms-bars">
@foreach ($goalMilestones as $ms)
<div class="gv-msb-row">
{{-- Status dot the monitoring signal, moved here
from the management list; it also carries the
status color when the bar sits at 0%. --}}
<span class="gv-msb-dot" style="background:{{ $ms['color'] }};" aria-hidden="true"></span>
<a class="gv-msb-name" href="#/tickets/editMilestone/{{ (int) $ms['id'] }}" title="{{ __('links.edit_milestone') }}: {{ $ms['headline'] }}">{{ $ms['headline'] }}</a>
<div class="gv-msb-track"><div class="gv-msb-fill" style="width:{{ (int) $ms['percentDone'] }}%;background:{{ $ms['color'] }};"></div></div>
<span class="gv-msb-pct">{{ (int) $ms['percentDone'] }}%</span>
</div>
@endforeach
</div>
@endif
</div>
{{-- ── Tab: Milestones ── --}}
@if ($id !== '')
<div class="gv-panel" data-panel="milestones" role="tabpanel" id="gvPanel-milestones" aria-labelledby="gvTab-milestones" tabindex="0">
{{-- Summary + chips live in a partial so the chip-remove
hx-post re-renders the whole section (counts + arrow
stay correct deleting only the chip left them stale). --}}
@include('goalcanvas::partials.milestonesSection')
@if ($login::userIsAtLeast($roles::$editor))
<div class="row" id="newMilestone" style="display:none;">
<div class="col-md-12">
<x-global::forms.text-input width="50%" name="newMilestone" /><br />
<input type="hidden" name="type" value="milestone" />
<input type="hidden" name="goalcanvasitemid" value="{{ $id }}" />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryCanvasSubmitButton').click()" contentRole="primary" />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.cancel')" onclick="leantime.goalCanvasController.toggleMilestoneSelectors('hide')" contentRole="tertiary" />
</div>
</div>
<div class="row" id="existingMilestone" style="display:none;">
<div class="col-md-12">
<select data-placeholder="{{ __("input.placeholders.filter_by_milestone") }}" name="existingMilestone" class="user-select">
<option value=""></option>
@foreach ($milestones as $milestoneRow)
<option value="{{ $milestoneRow->id }}">{{ $milestoneRow->headline }}</option>
@endforeach
</select>
<input type="hidden" name="type" value="milestone" />
<input type="hidden" name="goalcanvasitemid" value="{{ $id }}" />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.save')" onclick="jQuery('#primaryCanvasSubmitButton').click()" contentRole="primary" />
<x-global::forms.button tag="input" inputType="button" :labelText="__('buttons.cancel')" onclick="leantime.goalCanvasController.toggleMilestoneSelectors('hide')" contentRole="tertiary" />
</div>
</div>
@endif
</div>
@endif
{{-- ── Actions (main column; Delete lives in the Details rail) ── --}}
@if ($login::userIsAtLeast($roles::$editor))
<div class="gv-actions">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" id="primaryCanvasSubmitButton" />
<x-global::forms.button inputType="submit" contentRole="secondary" id="saveAndClose" value="closeModal"
onclick="leantime.goalCanvasController.setCloseModal();">{{ __('buttons.save_and_close') }}</x-global::forms.button>
</div>
@endif
</div>{{-- /gv-main --}}
{{-- ── Details rail the essentials (Docs-article pattern). ── --}}
<aside class="gv-side">
<h4 class="widgettitle title-light gv-side-head"><i class="fa fa-circle-info" aria-hidden="true"></i> {{ __('goalcanvas.side_details') }}</h4>
<div>
<label for="statusCanvas">{{ __('label.status') }}</label>
@if (!empty($statusLabels))
<select name="status" id="statusCanvas"></select>
@else
<input type="hidden" name="status" value="{{ $canvasItem['status'] ?? array_key_first($hiddenStatusLabels) }}" />
@endif
</div>
{{-- One label per field ("Due Dates" + "Start Date" + "End
Date" was triple-labeling part of the clutter). --}}
<div class="gv-dates">
<div>
<label for="goalStartDate">{{ __('label.start_date') }}</label>
<input type="text" autocomplete="off" id="goalStartDate" value="{{ format($canvasItem['startDate'])->date() }}" name="startDate" class="startDate"/>
</div>
<div>
<label for="goalEndDate">{{ __('label.end_date') }}</label>
<input type="text" autocomplete="off" id="goalEndDate" value="{{ format($canvasItem['endDate'])->date() }}" name="endDate" class="endDate"/>
</div>
</div>
<div>
@dispatchEvent('beforeMeasureGoalContainer', $canvasItem)
@if (!empty($relatesLabels))
<label class="gv-field-lbl" for="relatesCanvas">{{ __('label.relates') }}</label><select name="relates" id="relatesCanvas"></select>
@else
<input type="hidden" name="relates" value="{{ $canvasItem['relates'] ?? array_key_first($hiddenRelatesLabels) }}">
@endif
</div>
@if ($login::userIsAtLeast($roles::$editor) && $id != '')
<div class="gv-delete-slot">
<x-global::forms.button tag="a" link="{{ BASE_URL }}/goalcanvas/delCanvasItem/{{ $id }}" class="formModal delete gv-delete" state="danger" variant="outline">
<i class='fa fa-trash-can'></i> {{ __('links.delete') }}
</x-global::forms.button>
</div>
@endif
</aside>
</div>{{-- /gv-cols --}}
</form>
{{-- Discussion the comments submodule brings its OWN <form>; it must
stay a SIBLING of the goal form: nested, the HTML parser drops the
inner form tag and its close tag closes the OUTER form, orphaning
every field and button after it (Save did nothing). --}}
@if ($id !== '')
<div class="gv-discussion">
<hr />
<h4 class="widgettitle title-light"><span class="fa-solid fa-comments" aria-hidden="true"></span> {{ __('subtitles.discussion') }}</h4>
@include('comments::submodules.generalComment', ['formUrl' => '/goalcanvas/editCanvasItem/' . $id])
</div>
@endif
</div>
<script type="text/javascript">
jQuery(document).ready(function() {
leantime.dateController.initDateRangePicker(".startDate", ".endDate");
// Live unit cue on Start/Now/Goal so it's clear they're numbers.
(function () {
var typeSel = document.querySelector('.gvDialog select[name="metricType"]');
if (typeSel) {
var units = { number: '#', percent: '%', currency: '$' };
var apply = function () {
var u = units[typeSel.value] || '#';
document.querySelectorAll('.gvDialog .gv-unit').forEach(function (s) { s.textContent = '(' + u + ')'; });
};
apply();
typeSel.addEventListener('change', apply);
}
})();
// Tabs — one zone at a time; remembers the last-used tab.
(function () {
var tabs = document.querySelectorAll('.gvDialog .gv-tab');
var panels = document.querySelectorAll('.gvDialog .gv-panel');
if (!tabs.length) return;
function show(name) {
var found = false;
panels.forEach(function (p) { var m = p.getAttribute('data-panel') === name; p.hidden = !m; p.style.display = m ? '' : 'none'; if (m) found = true; });
tabs.forEach(function (t) {
var active = t.getAttribute('data-tab') === name;
t.classList.toggle('is-active', active);
t.setAttribute('aria-selected', active ? 'true' : 'false');
t.tabIndex = active ? 0 : -1;
});
if (found) { try { localStorage.setItem('gvActiveTab', name); } catch (e) {} }
return found;
}
tabs.forEach(function (t, i) {
t.addEventListener('click', function () { show(t.getAttribute('data-tab')); });
// Roving-tabindex arrow-key navigation (WAI-ARIA tabs pattern).
t.addEventListener('keydown', function (e) {
var next = null;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { next = tabs[(i + 1) % tabs.length]; }
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { next = tabs[(i - 1 + tabs.length) % tabs.length]; }
else if (e.key === 'Home') { next = tabs[0]; }
else if (e.key === 'End') { next = tabs[tabs.length - 1]; }
if (next) { e.preventDefault(); show(next.getAttribute('data-tab')); next.focus(); }
});
});
var saved = null; try { saved = localStorage.getItem('gvActiveTab'); } catch (e) {}
// Default = Progress: updating the value is the recurring job
// this dialog exists for (stale saved names fall through too).
if (!saved || !show(saved)) { if (!show('progress')) { show(tabs[0].getAttribute('data-tab')); } }
})();
@if (!empty($statusLabels))
new SlimSelect({
select: '#statusCanvas',
showSearch: false,
valuesUseText: false,
data: [
@foreach ($statusLabels as $key => $data)
@if ($data['active'])
{
innerHTML: '<i class="fas fa-fw {{ $data['icon'] }}"></i>&nbsp;{{ $data['title'] }}',
text: "{{ $data['title'] }}",
value: "{{ $key }}",
selected: {{ $canvasItem['status'] == $key ? 'true' : 'false' }}
},
@endif
@endforeach
]
});
@endif
@if (!empty($relatesLabels))
new SlimSelect({
select: '#relatesCanvas',
showSearch: false,
valuesUseText: false,
data: [
@foreach ($relatesLabels as $key => $data)
@if ($data['active'])
{
innerHTML: '<i class="fas fa-fw {{ $data['icon'] }}"></i>&nbsp;{{ $data['title'] }}',
text: "{{ $data['title'] }}",
value: "{{ $key }}",
selected: {{ $canvasItem['relates'] == $key ? 'true' : 'false' }}
},
@endif
@endforeach
]
});
@endif
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initSimpleEditor();
}
@if (!$login::userIsAtLeast($roles::$editor))
leantime.authController.makeInputReadonly(".nyroModalCont");
@endif
@if ($login::userHasRole([$roles::$commenter]))
leantime.commentsController.enableCommenterForms();
@endif
});
</script>
@endsection

View File

@@ -0,0 +1,355 @@
@extends($layout)
@section('content')
@php
use Leantime\Domain\Comments\Repositories\Comments;
use Leantime\Domain\Goalcanvas\Services\Goalcanvas;
$elementName = 'goal';
/**
* showCanvasTop.inc template - Top part of the main canvas page
*
* Required variables:
* - goal Name of current canvas
*/
$canvasTitle = '';
//get canvas title
foreach ($allCanvas as $canvasRow) {
if ($canvasRow["id"] == $currentCanvas) {
$canvasTitle = $canvasRow["title"];
$canvasId = $canvasRow["id"];
break;
}
}
@endphp
<style>
.canvas-row { margin-left: 0px; margin-right: 0px;}
.canvas-title-only { border-radius: var(--box-radius-small); }
h4.canvas-element-title-empty { background: white !important; border-color: white !important; }
div.canvas-element-center-middle { text-align: center; }
</style>
<div class="pageheader">
<div class="pageicon"><span class='fa {{ $canvasIcon }}'></span></div>
<div class="pagetitle">
@if (count($allCanvas) > 0)
<x-global::subjectSwitcher
:parent="__('headline.goal.dashboardboard')"
current="All Goal Groups">
@if ($login::userIsAtLeast($roles::$editor))
<li><a href="#/goalcanvas/bigRock">{!! __("links.icon.create_new_board") !!}</a></li>
@endif
<li class="border"></li>
@foreach ($allCanvas as $canvasRow)
<li><a href="{{ BASE_URL }}/goalcanvas/showCanvas/{{ $canvasRow['id'] }}">{{ $canvasRow['title'] }}</a></li>
@endforeach
</x-global::subjectSwitcher>
@else
<h1>{{ __("headline.goal.dashboardboard") }}</h1>
@endif
</div>
</div><!--pageheader-->
<div class="maincontent">
<div class="row" style="margin-bottom:20px; ">
<div class="col-md-4">
<div class="bigNumberBox" style="padding: 29px 15px;">
<h2>Progress: {{ round($goalStats['avgPercentComplete']) }}%</h2>
<div class="progress" style="margin-top:5px;">
<div class="progress-bar progress-bar-success" role="progressbar"
aria-valuenow="{{ round($goalStats['avgPercentComplete']) }}" aria-valuemin="0" aria-valuemax="100"
style="width: {{ $goalStats['avgPercentComplete'] }}%">
<span class="sr-only">{{ sprintf(__("text.percent_complete"), round($goalStats['avgPercentComplete'])) }}</span>
</div>
</div>
</div>
</div>
<div class="col-md-2"></div>
<div class="col-md-2">
<div class="bigNumberBox priority-border-4">
<h2>On Track</h2>
<span class="content">{{ $goalStats['goalsOnTrack'] }}</span>
</div>
</div>
<div class="col-md-2">
<div class="bigNumberBox priority-border-3">
<h2>At Risk</h2>
<span class="content">{{ $goalStats['goalsAtRisk'] }}</span>
</div>
</div>
<div class="col-md-2">
<div class="bigNumberBox priority-border-1">
<h2>Miss</h2>
<span class="content">{{ $goalStats['goalsMiss'] }}</span>
</div>
</div>
</div>
<div class="maincontentinner">
<div class="row">
<div class="col-md-6"></div>
</div>
@if (count($allCanvas) > 0)
@foreach ($allCanvas as $canvasRow)
<div class="row">
<div class="col-md-12">
<a href='#/goalcanvas/editCanvasItem?type=goal&canvasId={{ $canvasRow["id"] }}' class='btn btn-primary pull-right'><i class="fa fa-plus"></i> Create New Goal</a>
<h5 class='subtitle'><a href='{{ BASE_URL }}/goalcanvas/showCanvas/{{ $canvasRow["id"] }}'>{{ $canvasRow["title"] }}</a></h5>
</div>
</div>
<div class="row" style="border-bottom:1px solid var(--main-border-color); margin-bottom:20px">
@php
$canvasSvc = app()->make(Goalcanvas::class);
$canvasItems = $canvasSvc->getCanvasItemsById($canvasRow["id"]);
@endphp
<div id="sortableCanvasKanban-{{ $canvasRow['id'] }}" class="sortableTicketList disabled col-md-12" style="padding-top:15px;">
<div class="row">
<div class="col-md-12">
<div class="row">
@if (!is_countable($canvasItems) || count($canvasItems) == 0)
<div class='col-md-12'>No goals on this board yet. Open the <a href='{{ BASE_URL }}/goalcanvas/showCanvas/{{ $canvasRow["id"] }}'>board</a> to start adding goals</div>
@endif
@foreach ($canvasItems as $row)
@php
$filterStatus = $filter['status'] ?? 'all';
$filterRelates = $filter['relates'] ?? 'all';
@endphp
@if ($row['box'] === $elementName && ($filterStatus == 'all' || $filterStatus == $row['status']) && ($filterRelates == 'all' || $filterRelates == $row['relates']))
@php
$comments = app()->make(Comments::class);
$nbcomments = $comments->countComments(moduleId: $row['id']);
@endphp
<div class="col-md-4">
<div class="ticketBox" id="item_{{ $row["id"] }}">
<div class="row">
<div class="col-md-12">
<div class="inlineDropDownContainer" style="float:right;">
@if ($login::userIsAtLeast($roles::$editor))
<a href="javascript:void(0)" class="dropdown-toggle ticketDropDown" data-toggle="dropdown">
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
</a>
@endif
@if ($login::userIsAtLeast($roles::$editor))
&nbsp;&nbsp;&nbsp;
<ul class="dropdown-menu">
<li class="nav-header">{{ __("subtitles.edit") }}</li>
<li><a href="#/goalcanvas/editCanvasItem/{{ $row["id"] }}" class="goalCanvasModal" data="item_{{ $row["id"] }}">{!! __("links.edit_canvas_item") !!}</a></li>
<li><a href="#/goalcanvas/delCanvasItem/{{ $row["id"] }}" class="delete goalCanvasModal" data="item_{{ $row["id"] }}">{!! __("links.delete_canvas_item") !!}</a></li>
</ul>
@endif
</div>
<h4>
<strong>Goal:</strong>
<a href="#/goalcanvas/editCanvasItem/{{ $row['id'] }}" class="goalCanvasModal" data="item_{{ $row['id'] }}">
{{ $row["title"] }}
</a>
</h4>
<br />
<strong>Metric:</strong> {{$row["description"] }}
<br /><br />
@php
$percentDone = $row["goalProgress"];
$metricTypeFront = '';
$metricTypeBack = '';
if ($row["metricType"] == "percent") {
$metricTypeBack = '%';
} elseif ($row["metricType"] == "currency") {
$metricTypeFront = __("language.currency");
}
@endphp
<div class="row">
<div class="col-md-4"></div>
<div class="col-md4 center">
<small>{{ sprintf(__("text.percent_complete"), $percentDone) }}</small>
</div>
<div class="col-md-4"></div>
</div>
<div class="progress" style="margin-bottom:0px;">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{ $percentDone }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ $percentDone }}%">
<span class="sr-only">{{ sprintf(__("text.percent_complete"), $percentDone) }}</span>
</div>
</div>
<div class="row" style="padding-bottom:0px;">
<div class="col-md-4">
<small>Start:<br />{{ $metricTypeFront . $row["startValue"] . $metricTypeBack }}</small>
</div>
<div class="col-md-4 center">
<small>{{ __('label.current') }}:<br />{{ $metricTypeFront . $row["currentValue"] . $metricTypeBack }}</small>
</div>
<div class="col-md-4" style="text-align:right">
<small>{{ __('label.goal') }}:<br />{{ $metricTypeFront . $row["endValue"] . $metricTypeBack }}</small>
</div>
</div>
<div class="clearfix" style="padding-bottom: 8px;"></div>
@if (!empty($statusLabels))
<div class="dropdown ticketDropdown statusDropdown colorized show firstDropdown">
<a class="dropdown-toggle f-left status label-{{ $row['status'] != "" ? $statusLabels[$row['status']]['dropdown'] : "" }}" href="javascript:void(0);" role="button" id="statusDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="text">{{ $row['status'] != "" ? $statusLabels[$row['status']]['title'] : "" }}</span> <i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="statusDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">{{ __("dropdown.choose_status") }}</li>
@foreach ($statusLabels as $key => $data)
@if ($data['active'] || true)
<li class='dropdown-item'>
<a href="javascript:void(0);" class="label-{{ $data['dropdown'] }}" data-label='{{ $data["title"] }}' data-value="{{ $row['id'] . "/" . $key }}" id="ticketStatusChange{{ $row['id'] . $key }}">{!! $data['title'] !!}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
@if (!empty($relatesLabels))
<div class="dropdown ticketDropdown relatesDropdown colorized show firstDropdown">
<a class="dropdown-toggle f-left relates label-{{ $relatesLabels[$row['relates']]['dropdown'] }}" href="javascript:void(0);" role="button" id="relatesDropdownMenuLink{{ $row['id'] }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="text">{{ $relatesLabels[$row['relates']]['title'] }}</span> <i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu" aria-labelledby="relatesDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">{{ __("dropdown.choose_relates") }}</li>
@foreach ($relatesLabels as $key => $data)
@if ($data['active'] || true)
<li class='dropdown-item'>
<a href="javascript:void(0);" class="label-{{ $data['dropdown'] }}" data-label='{{ $data["title"] }}' data-value="{{ $row['id'] . "/" . $key }}" id="ticketRelatesChange{{ $row['id'] . $key }}">{!! $data['title'] !!}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
<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">
@if ($row["authorFirstname"] != "")
<span id='userImage{{ $row['id'] }}'>
<img src='{{ BASE_URL }}/api/users?profileImage={{ $row['author'] }}' width='25' style='vertical-align: middle;'/>
</span>
<span id='user{{ $row['id'] }}'></span>
@else
<span id='userImage{{ $row['id'] }}'>
<img src='{{ BASE_URL }}/api/users?profileImage=false' width='25' style='vertical-align: middle;'/>
</span>
<span id='user{{ $row['id'] }}'></span>
@endif
</span>
</a>
<ul class="dropdown-menu" aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">{{ __("dropdown.choose_user") }}</li>
@foreach ($users as $user)
<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>
</li>
@endforeach
</ul>
</div>
<div class="right" style="margin-right:10px;">
<span class="fas fa-comments"></span>
<small>{{ $nbcomments }}</small>
</div>
</div>
</div>
@include('goalcanvas::partials.milestoneChips', ['milestones' => $row['milestones'] ?? []])
</div>
</div>
@endif
@endforeach
</div>
<br />
</div>
</div>
</div>
</div>
@endforeach
@endif
</div>
{{--
* showCanvasBottom.blade.php template - Bottom part of the main canvas page
*
* Required variables:
* - goal Name of current canvas
--}}
@if (count($allCanvas) > 0)
{{-- --}}
@else
<br><br>
<div class='center'>
<div class='svgContainer'>
{!! file_get_contents(ROOT . "/dist/images/svg/undraw_design_data_khdb.svg") !!}
</div>
<h3>{{ __("headlines.goal.analysis") }}</h3>
<br>{!! __("text.goal.helper_content") !!}
@if ($login::userIsAtLeast($roles::$editor))
<br><br>
<a href='javascript:void(0)' class='addCanvasLink btn btn-primary'>
{{ __("links.icon.create_new_board") }}
</a>
@endif
</div>
@endif
@if (!empty($disclaimer) && count($allCanvas) > 0)
<small class="align-center">{{ $disclaimer }}</small>
@endif
{!! $tpl->viewFactory->make($tpl->getTemplatePath('canvas', 'modals'), $__data)->render() !!}
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function() {
if(jQuery('#searchCanvas').length > 0) {
new SlimSelect({ select: '#searchCanvas' });
}
leantime.goalCanvasController.setRowHeights();
leantime.canvasController.setCanvasName('goal');
leantime.canvasController.initFilterBar();
@if ($login::userIsAtLeast($roles::$editor))
leantime.canvasController.initCanvasLinks();
leantime.canvasController.initUserDropdown();
leantime.canvasController.initStatusDropdown();
leantime.canvasController.initRelatesDropdown();
@else
leantime.authController.makeInputReadonly(".maincontentinner");
@endif
@if (isset($_GET['showModal']))
@php
$modalUrl = $_GET['showModal'] == ""
? "&type=" . array_key_first($canvasTypes)
: "/" . (int)$_GET['showModal'];
@endphp
leantime.canvasController.openModalManually("{{ BASE_URL }}/goalcanvas/editCanvasItem{{ $modalUrl }}");
window.history.pushState({}, document.title, '{{ BASE_URL }}/goalcanvas/showCanvas/');
@endif
});
</script>
@endsection

View File

@@ -0,0 +1,15 @@
@extends($layout)
@section('content')
<h4 class="widgettitle title-light">{!!__("subtitles.delete") !!}</h4>
<form method="post" action="{{ BASE_URL."/goalcanvas/delCanvas/$id" }}">
@if(isset($csrf_token))
<input type="hidden" name="csrf_token" value="{{ $csrf_token }}">
@endif
<p>{{ __('text.confirm_board_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 }}/goalcanvas/showCanvas">{{ __('buttons.back') }}</x-global::forms.button>
</form>
@endsection

View File

@@ -0,0 +1,12 @@
@extends($layout)
@section('content')
<h4 class="widgettitle title-light">{!! __("subtitles.delete") !!}</h4>
<hr style="margin-top: 5px; margin-bottom: 15px;">
<form method="post" action="{{ BASE_URL }}/goalcanvas/delCanvasItem/{{ $id }}">
<p>{{ __('text.confirm_board_item_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 }}/goalcanvas/showCanvas">{{ __('buttons.back') }}</x-global::forms.button>
</form>
@endsection

View File

@@ -0,0 +1,32 @@
{{--
Milestone chip row for a goal (board + dashboard).
Expects: $milestones = [{ id, headline, color, percentDone, statusType }, ...]
The fill is the milestone's OWN color growing with progress — deliberately
not a status color (status lives on the goal card, never the chips).
Each chip links to its milestone; stopPropagation keeps the click from
also opening the goal card behind it.
--}}
@if (! empty($milestones))
{{-- This partial is included once per goal card; emit the shared chip CSS
only on the first include so a board of N cards doesn't ship N copies. --}}
@once
<style>
.goalMsBoardRow{scrollbar-width:thin;scrollbar-color:#9aa7ad transparent;}
.goalMsBoardRow::-webkit-scrollbar{height:8px;}
.goalMsBoardRow::-webkit-scrollbar-thumb{background:#9aa7ad;border-radius:10px;border:2px solid transparent;background-clip:padding-box;}
.goalMsBoardRow::-webkit-scrollbar-track{background:transparent;}
.goalMsBoardChip{text-decoration:none;color:inherit;cursor:pointer;transition:border-color .12s;}
.goalMsBoardChip:hover{border-color:var(--primary-color,#004666)!important;}
</style>
@endonce
<div class="goalMsBoardRow" style="display:flex;gap:6px;overflow-x:auto;padding-bottom:4px;margin-top:6px;">
@foreach ($milestones as $ms)
<a href="#/tickets/editMilestone/{{ (int) $ms['id'] }}" onclick="event.stopPropagation();" class="goalMsBoardChip" title="{{ __('links.edit_milestone') }}: {{ $ms['headline'] }}"
style="position:relative;flex:0 0 auto;min-width:120px;max-width:180px;height:32px;border-radius:8px;border:1px solid var(--main-border-color,#e4e7ec);background:var(--secondary-background,#f2f4f7);overflow:hidden;display:flex;align-items:center;padding:0 9px;">
<span style="position:absolute;left:0;top:0;bottom:0;width:{{ (int) $ms['percentDone'] }}%;background:{{ $ms['color'] }};opacity:.18;border-right:2px solid {{ $ms['color'] }};"></span>
<span style="position:relative;z-index:1;flex:1;min-width:0;font-size:11.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ $ms['headline'] }}</span>
<span style="position:relative;z-index:1;flex:none;font-size:10px;font-weight:600;opacity:.65;margin-left:5px;">{{ (int) $ms['percentDone'] }}%</span>
</a>
@endforeach
</div>
@endif

View File

@@ -0,0 +1,62 @@
{{--
Goal editor linked-milestones MANAGEMENT list (summary + one row per
milestone). Progress bars deliberately live on the Progress tab, not here
(review 2026-08-03: chips carrying percent duplicated the Progress view)
this list is for linking/unlinking, RA line-item style.
Lives in its own partial so the row-remove hx-post can re-render the WHOLE
section (hx-target="#goalMsSection" outerHTML): the summary count stays
correct with the removal.
Expects: $id, $goalMilestones, $milestoneSummary, $milestones (project
milestone options drives the "link existing" button visibility).
--}}
<div id="goalMsSection">
<div class="gv-ms-head">
@if (($milestoneSummary['total'] ?? 0) > 0)
<span class="gv-ms-summary"><b>{{ $milestoneSummary['total'] }}</b> {{ $milestoneSummary['total'] == 1 ? __("goalcanvas.summary_milestone_one") : __("goalcanvas.summary_milestones") }}
@if ($milestoneSummary['inProgress'] > 0)&middot; {{ $milestoneSummary['inProgress'] }} {{ __("goalcanvas.summary_in_progress") }} @endif
@if ($milestoneSummary['notStarted'] > 0)&middot; {{ $milestoneSummary['notStarted'] }} {{ __("goalcanvas.summary_not_started") }} @endif
@if ($milestoneSummary['done'] > 0)&middot; {{ $milestoneSummary['done'] }} {{ __("goalcanvas.summary_done") }} @endif
</span>
@endif
<span class="gv-ms-actions">
@if ($login::userIsAtLeast($roles::$editor))
<button type="button" class="gv-ms-act helperTooltip" onclick="leantime.goalCanvasController.toggleMilestoneSelectors('new');" data-tippy-content="{{ __('goalcanvas.ms_new') }}" title="{{ __('goalcanvas.ms_new') }}" aria-label="{{ __('goalcanvas.ms_new') }}"><i class="fa fa-plus" aria-hidden="true"></i></button>
@if (count($milestones) > 0)
<button type="button" class="gv-ms-act helperTooltip" onclick="leantime.goalCanvasController.toggleMilestoneSelectors('existing');" data-tippy-content="{{ __('goalcanvas.ms_link') }}" title="{{ __('goalcanvas.ms_link') }}" aria-label="{{ __('goalcanvas.ms_link') }}"><i class="fa fa-link" aria-hidden="true"></i></button>
@endif
@endif
<i class="fa fa-question-circle-o helperTooltip" aria-hidden="true" data-tippy-content="{{ __("tooltip.link_milestones_tooltip") }}"></i>
</span>
</div>
@if (count($goalMilestones) > 0)
<div class="gv-ms-list">
@foreach ($goalMilestones as $ms)
@php
$msDue = trim((string) ($ms['editTo'] ?? ''));
$msDue = ($msDue === '' || str_starts_with($msDue, '0000-00-00')) ? null : $msDue;
@endphp
{{-- Management row only status signals (dots/bars) live on
the Progress tab (review 2026-08-04). --}}
<div class="gv-ms-item">
<a class="gv-ms-name" href="#/tickets/editMilestone/{{ (int) $ms['id'] }}" title="{{ __('links.edit_milestone') }}: {{ $ms['headline'] }}">{{ $ms['headline'] }}</a>
@if ($msDue !== null)
<span class="gv-ms-due">{{ __('label.due') }} {{ format($msDue)->date() }}</span>
@endif
@if ($login::userIsAtLeast($roles::$editor))
<button type="button"
hx-post="{{ BASE_URL }}/goalcanvas/editCanvasItem/{{ $id }}"
hx-vals='{"removeMilestone": {{ (int) $ms['id'] }}}'
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
hx-target="#goalMsSection"
hx-swap="outerHTML"
class="delete gv-ms-remove"
aria-label="{{ __("links.remove") }}: {{ $ms['headline'] }}" title="{{ __("links.remove") }}"><i class="fa fa-close" aria-hidden="true"></i></button>
@endif
</div>
@endforeach
</div>
@endif
</div>

View File

@@ -0,0 +1,103 @@
{{--
Goal metric readout RA planbar language: filled progress + striped
remaining, quarter score marks, a position marker at the current value,
start/goal anchored under the bar's ends, and an explicit "N to go".
The number IS the input (RA inline pattern): blur-when-changed hx-posts
to goalProgress/updateValue, which re-renders this partial into #gvReadout.
Expects: $canvasItem (id, metricType, startValue, currentValue, endValue,
setting, description). Shared by the dialog and the HxController.
--}}
@php
$roType = $canvasItem['metricType'] ?: 'number';
$roStart = (float) ($canvasItem['startValue'] ?? 0);
$roCur = (float) ($canvasItem['currentValue'] ?? 0);
$roGoal = (float) ($canvasItem['endValue'] ?? 0);
$roHasRange = ($roGoal - $roStart) != 0;
$roPct = $roHasRange ? max(0, min(100, (($roCur - $roStart) / ($roGoal - $roStart)) * 100)) : 0;
$roFmt = function ($v) use ($roType) {
$v = (float) $v;
if ($roType === 'percent') {
return rtrim(rtrim(number_format($v, 2), '0'), '.').'%';
}
if ($roType === 'currency') {
return '$'.number_format($v, $v == floor($v) ? 0 : 2);
}
return rtrim(rtrim(number_format($v, 2), '0'), '.');
};
$roReached = $roHasRange && $roPct >= 100;
// Works for decreasing goals too (goal < start): distance left to travel.
$roRemaining = abs($roGoal - $roCur);
// linkAndReport current values are computed from children; viewers can't
// write — both render the number as static text instead of an input.
$roEditable = $login::userIsAtLeast($roles::$editor) && ($canvasItem['setting'] ?? '') !== 'linkAndReport';
// Inline blur-save needs a PERSISTED goal: on the create form (id empty)
// an hx-post with itemId=0 would swap the readout back to zeros and wipe
// the value the user just typed — new goals get a plain input that
// submits with the create form instead.
$roLive = (int) ($canvasItem['id'] ?? 0) > 0;
@endphp
<div class="gv-metric-bar" id="gvReadout">
{{-- WHAT is being measured without it the tab is a bare number. --}}
@if (trim((string) ($canvasItem['description'] ?? '')) !== '')
<div class="gv-mb-metric">{{ $canvasItem['description'] }}</div>
@endif
<div class="gv-mb-top">
@if ($roEditable && $roLive)
<input class="gv-mb-input" type="number" step="0.01" name="currentValue"
value="{{ $roCur == floor($roCur) ? (int) $roCur : $roCur }}"
aria-label="{{ __('goalcanvas.update_current') }}"
data-tippy-content="{{ __('goalcanvas.update_current') }}"
hx-post="{{ BASE_URL }}/hx/goalcanvas/goalProgress/updateValue"
hx-vals='{"itemId": {{ (int) $canvasItem['id'] }}}'
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
hx-trigger="blur changed"
hx-target="#gvReadout"
hx-swap="outerHTML">
@elseif ($roEditable)
{{-- New goal: same editable number, no HTMX the value rides the
create form's own submit. --}}
<input class="gv-mb-input" type="number" step="0.01" name="currentValue"
value="{{ $roCur == floor($roCur) ? (int) $roCur : $roCur }}"
aria-label="{{ __('goalcanvas.update_current') }}"
data-tippy-content="{{ __('goalcanvas.update_current') }}">
@else
<span class="gv-mb-now" @if (($canvasItem['setting'] ?? '') === 'linkAndReport') data-tippy-content="{{ __('text.current_value_calculated_from_children') }}" @endif>{{ $roFmt($roCur) }}</span>
{{-- Static readout (linkAndReport / viewer) submits no currentValue,
and EditCanvasItem defaults an absent one to '' — which would
blank the stored metric when the user saves other fields. Round
the stored value back through the form to preserve it. --}}
<input type="hidden" name="currentValue" value="{{ $roCur == floor($roCur) ? (int) $roCur : $roCur }}">
@endif
{{-- How far away it ISN'T the missing half of every progress bar. --}}
@if ($roHasRange)
<span class="gv-mb-togo">
@if ($roReached)
{{ __('goalcanvas.goal_reached') }}
@else
{{ sprintf(__('goalcanvas.to_go'), $roFmt($roRemaining)) }}
@endif
</span>
@endif
</div>
{{-- Scored track (RA planbar language) + resolved % the % column
width/gap matches the milestone rows so both right edges align. --}}
<div class="gv-mb-barrow">
<div class="gv-mb-scalewrap">
<div class="gv-track gv-track--scored" aria-hidden="true">
<div class="gv-fill" style="width:{{ $roPct }}%"></div>
<span class="gv-tick" style="left:25%"></span>
<span class="gv-tick" style="left:50%"></span>
<span class="gv-tick" style="left:75%"></span>
@if ($roPct > 0 && $roPct < 100)
<span class="gv-marker" style="left:{{ $roPct }}%"></span>
@endif
</div>
{{-- The totals live where they ARE on the scale: start under the
left end, the goal under the right end. --}}
<div class="gv-scale"><span>{{ $roFmt($roStart) }}</span><span>{{ $roFmt($roGoal) }}</span></div>
</div>
<span class="gv-mb-pct">{{ (int) round($roPct) }}%</span>
</div>
</div>

View File

@@ -0,0 +1,66 @@
@if (count($allCanvas) > 0)
@else
<br /><br />
<div class='center'>
<div class='svgContainer'>
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_design_data_khdb.svg') !!}
</div>
<h3>{{ __("headlines.goal.analysis") }}</h3>
<br />{{ __("text.goal.helper_content") }}
@if ($login::userIsAtLeast($roles::$editor))
<br /><br />
<a href='javascript:void(0)' class='addCanvasLink btn btn-primary'>
{{ __('links.icon.create_new_board') }}
</a>
@endif
</div>
@endif
@if (!empty($disclaimer) && count($allCanvas) > 0)
<small class="align-center">{{ $disclaimer }}</small>
@endif
{!! $tpl->viewFactory->make($tpl->getTemplatePath('canvas', 'modals'), $__data)->render() !!}
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function() {
if (jQuery('#searchCanvas').length > 0) {
new SlimSelect({
select: '#searchCanvas'
});
}
leantime.goalCanvasController.setRowHeights();
leantime.canvasController.setCanvasName('goal');
leantime.canvasController.initFilterBar();
@if ($login::userIsAtLeast($roles::$editor))
leantime.canvasController.initCanvasLinks();
leantime.canvasController.initUserDropdown();
leantime.canvasController.initStatusDropdown();
leantime.canvasController.initRelatesDropdown();
@else
leantime.authController.makeInputReadonly(".maincontentinner");
@endif
@if (isset($_GET['showModal']))
@php
if ($_GET['showModal'] == "") {
$modalUrl = "&type=" . array_key_first($canvasTypes);
} else {
$modalUrl = "/" . (int)$_GET['showModal'];
}
@endphp
leantime.canvasController.openModalManually(
"{{ BASE_URL }}/goalcanvas/editCanvasItem{{ $modalUrl }}");
window.history.pushState({}, document.title, '{{ BASE_URL }}/goalcanvas/showCanvas/');
@endif
});
</script>

View File

@@ -0,0 +1,485 @@
@extends($layout)
@section('content')
@php
$elementName = 'goal';
@endphp
@php
$canvasTitle = '';
//get canvas title
foreach ($allCanvas as $canvasRow) {
if ($canvasRow['id'] == $currentCanvas) {
$canvasTitle = $canvasRow['title'];
break;
}
}
@endphp
<style>
.canvas-row {
margin-left: 0px;
margin-right: 0px;
}
.canvas-title-only {
border-radius: var(--box-radius-small);
}
h4.canvas-element-title-empty {
background: white !important;
border-color: white !important;
}
div.canvas-element-center-middle {
text-align: center;
}
</style>
<div class="pageheader">
<div class="pageicon"><span class="fas {{ $canvasIcon }}"></span></div>
<div class="pagetitle">
@if (count($allCanvas) > 0)
<x-global::subjectSwitcher
:parent="__('headline.goal.board')"
:current="$canvasTitle">
@if ($login::userIsAtLeast($roles::$editor))
<li><a href="#/goalcanvas/bigRock">{!! __('links.icon.create_new_bigrock') !!}</a></li>
@endif
<li class="border"></li>
@foreach ($allCanvas as $canvasRow)
<li><a
href='{{ BASE_URL }}/goalcanvas/showCanvas/{{ $canvasRow['id'] }}'>{{ $canvasRow['title'] }}</a>
</li>
@endforeach
</x-global::subjectSwitcher>
@else
<h1>{{ __('headline.goal.board') }}</h1>
@endif
</div>
@if (count($allCanvas) > 0)
<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">
@if ($login::userIsAtLeast($roles::$editor))
<li><a href="#/goalcanvas/bigRock/{{ $currentCanvas }}">{!! __('links.icon.edit') !!}</a></li>
<li><a href="javascript:void(0)" class="cloneCanvasLink ">{!! __('links.icon.clone') !!}</a></li>
<li><a href="javascript:void(0)" class="mergeCanvasLink ">{!! __('links.icon.merge') !!}</a></li>
<li><a href="javascript:void(0)" class="importCanvasLink ">{!! __('links.icon.import') !!}</a>
</li>
@endif
<li><a
href="{{ BASE_URL }}/goalcanvas/export/{{ $currentCanvas }}">{!! __('links.icon.export') !!}</a>
</li>
<li><a href="javascript:window.print();">{!! __('links.icon.print') !!}</a></li>
@if ($login::userIsAtLeast($roles::$editor))
<li><a href="#/goalcanvas/delCanvas/{{ $currentCanvas }}"
class="delete">{!!__('links.icon.delete') !!}</a></li>
@endif
</ul>
</span>
</div>
@endif
</div>
<!--pageheader-->
<div class="maincontent">
<div class="maincontentinner">
<?php echo $tpl->displayNotification(); ?>
<div class="row">
<div class="col-md-3">
@if ($login::userIsAtLeast($roles::$editor) && count($canvasTypes) == 1 && count($allCanvas) > 0)
<x-global::forms.button tag="a" link="#/goalcanvas/editCanvasItem?type={{ $elementName }}" contentRole="primary"
id="{{ $elementName }}">{!! __('links.add_new_canvas_itemgoal') !!}</x-global::forms.button>
@endif
</div>
<div class="col-md-6 center">
</div>
<div class="col-md-3">
<div class="pull-right">
<div class="btn-group viewDropDown">
@if (count($allCanvas) > 0 && !empty($statusLabels))
@php
$filterStatus = $filter['status'] ?? 'all';
$filterRelates = $filter['relates'] ?? 'all';
@endphp
@if (($filterStatus ?? '') == 'all')
<button class="btn dropdown-toggle" data-toggle="dropdown"><i class="fas fa-filter"></i>
{!! __('status.all') !!} {!! __('links.view') !!}</button>
@else
<button class="btn dropdown-toggle" data-toggle="dropdown"><i
class="fas fa-fw {{ __($statusLabels[$filterStatus]['icon']) }}"></i>
{{ $statusLabels[$filterStatus]['title'] }} {{ __('links.view') }}</button>
@endif
<ul class="dropdown-menu">
<li><a href="{{ BASE_URL }}/goalcanvas/showCanvas?filter_status=all" @if ($filterStatus == 'all')
class="active"
@endif><i class="fas fa-globe"></i> {!! __('status.all') !!}</a></li>
@foreach ($statusLabels as $key => $data)
<li><a href="{{ BASE_URL }}/goalcanvas/showCanvas?filter_status={{ $key }}"
@if ($filterStatus == $key)
class="active"
@endif><i class="fas fa-fw {{ $data['icon'] }}"></i>
{!! $data['title'] !!}</a></li>
@endforeach
</ul>
@endif
</div>
<div class="btn-group viewDropDown">
@if (count($allCanvas) > 0 && !empty($relatesLabels))
@php
$filterStatus = $filter['status'] ?? 'all';
$filterRelates = $filter['relates'] ?? 'all';
@endphp
@if ($filterRelates == 'all')
<button class="btn dropdown-toggle" data-toggle="dropdown"><i
class="fas fa-fw fa-globe"></i> {{ __('relates.all') }}
{{ __('links.view') }}</button>
@else
<button class="btn dropdown-toggle" data-toggle="dropdown"><i
class="fas fa-fw {{ __($relatesLabels[$filterRelates]['icon']) }}"></i>
{{ $relatesLabels[$filterRelates]['title'] }} {{ __('links.view') }}</button>
@endif
<ul class="dropdown-menu">
<li><a href="{{ BASE_URL }}/goalcanvas/showCanvas?filter_relates=all" @if ($filterRelates == 'all')
class="active"
@endif><i class="fas fa-globe"></i> {{ __('relates.all') }}</a></li>
@foreach ($relatesLabels as $key => $data)
<li><a href="{{ BASE_URL }}/goalcanvas/showCanvas?filter_relates={{ $key }}"
@if ($filterRelates == $key)
class="active"
@endif><i class="fas fa-fw {{ $data['icon'] }}"></i>
{{ $data['title'] }}</a></li>
@endforeach
</ul>
@endif
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
@if (count($allCanvas) > 0)
<div id="sortableCanvasKanban" class="sortableTicketList disabled" style="padding-top:15px;">
<div class="row">
<div class="col-md-12">
<div class="row">
@foreach ($canvasItems as $row)
@php
$filterStatus = $filter['status'] ?? 'all';
$filterRelates = $filter['relates'] ?? 'all';
@endphp
@if (
$row['box'] === $elementName &&
($filterStatus == 'all' || $filterStatus == $row['status']) &&
($filterRelates == 'all' || $filterRelates == $row['relates']))
@php
$comments = app()->make(\Leantime\Domain\Comments\Repositories\Comments::class);
$nbcomments = $comments->countComments(moduleId: $row['id']);
@endphp
<div class="col-md-4">
<div class="ticketBox" id="item_{{ $row['id'] }}">
<div class="row">
<div class="col-md-12">
<div class="inlineDropDownContainer" style="float:right;">
@if ($login::userIsAtLeast($roles::$editor))
<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.edit') }}</li>
<li><a href="#/goalcanvas/editCanvasItem/{{ $row['id'] }}"
data="item_{{ $row['id'] }}">
{!! __('links.edit_canvas_item') !!}</a></li>
<li><a href="#/goalcanvas/delCanvasItem/{{ $row['id'] }}"
data="item_{{ $row['id'] }}">
{!! __('links.delete_canvas_item') !!}</a></li>
</ul>
@endif
</div>
<h4><strong>Goal:</strong> <a
href="#/goalcanvas/editCanvasItem/{{ $row['id'] }}"
data="item_{{ $row['id'] }}">{{ $row['title'] }}</a>
</h4>
<br />
<strong>Metric:</strong> {{ $row['description'] }}
<br /><br />
@php
$percentDone = $row['goalProgress'];
$metricTypeFront = '';
$metricTypeBack = '';
if ($row['metricType'] == 'percent') {
$metricTypeBack = '%';
} elseif ($row['metricType'] == 'currency') {
$metricTypeFront = __('language.currency');
}
@endphp
<div class="row">
<div class="col-md-4"></div>
<div class="col-md4 center">
<small>{{ sprintf(__('text.percent_complete'), $percentDone) }}</small>
</div>
<div class="col-md-4"></div>
</div>
<div class="progress" style="margin-bottom:0px;">
<div class="progress-bar progress-bar-success"
role="progressbar" aria-valuenow="{{ $percentDone }}"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ $percentDone }}%">
<span
class="sr-only">{{ sprintf(__('text.percent_complete'), $percentDone) }}</span>
</div>
</div>
<div class="row" style="padding-bottom:0px;">
<div class="col-md-4">
<small>Start:<br />{{ $metricTypeFront . $row['startValue'] . $metricTypeBack }}</small>
</div>
<div class="col-md-4 center">
<small>{{ __('label.current') }}:<br />{{ $metricTypeFront . $row['currentValue'] . $metricTypeBack }}</small>
</div>
<div class="col-md-4" style="text-align:right">
<small>{{ __('label.goal') }}:<br />{{ $metricTypeFront . $row['endValue'] . $metricTypeBack }}</small>
</div>
</div>
<div class="clearfix" style="padding-bottom: 8px;"></div>
@if (!empty($statusLabels))
<div
class="dropdown ticketDropdown statusDropdown colorized show firstDropdown">
<a class="dropdown-toggle f-left status label-{{ $row['status'] != '' ? $statusLabels[$row['status']]['dropdown'] : '' }}"
href="javascript:void(0);" role="button"
id="statusDropdownMenuLink{{ $row['id'] }}"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span
class="text">{{ $row['status'] != '' ? $statusLabels[$row['status']]['title'] : '' }}</span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu"
aria-labelledby="statusDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">
{{ __('dropdown.choose_status') }}</li>
@foreach ($statusLabels as $key => $data)
@if ($data['active'] || true)
<li class='dropdown-item'>
<a href="javascript:void(0);"
class="label-{{ $data['dropdown'] }}"
data-label='{{ $data['title'] }}'
data-value="{{ $row['id'] . '/' . $key }}"
id="ticketStatusChange{{ $row['id'] . $key }}">{{ $data['title'] }}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
@if (!empty($relatesLabels))
<div
class="dropdown ticketDropdown relatesDropdown colorized show firstDropdown">
<a class="dropdown-toggle f-left relates label-{{ $relatesLabels[$row['relates']]['dropdown'] }}"
href="javascript:void(0);" role="button"
id="relatesDropdownMenuLink{{ $row['id'] }}"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span
class="text">{{ $relatesLabels[$row['relates']]['title'] }}</span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu"
aria-labelledby="relatesDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">
{{ __('dropdown.choose_relates') }}</li>
@foreach ($relatesLabels as $key => $data)
@if ($data['active'] || true)
<li class='dropdown-item'>
<a href="javascript:void(0);"
class="label-{{ $data['dropdown'] }}"
data-label='{{ $data['title'] }}'
data-value="{{ $row['id'] . '/' . $key }}"
id="ticketRelatesChange{{ $row['id'] . $key }}">{{ $data['title'] }}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
<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">
@if ($row['authorFirstname'] != '')
<span id='userImage{{ $row['id'] }}'>
<img src='{{ BASE_URL }}/api/users?profileImage={{ $row['author'] }}'
width='25'
style='vertical-align: middle;' />
</span>
<span id='user{{ $row['id'] }}'></span>
@else
<span id='userImage{{ $row['id'] }}'>
<img src='{{ BASE_URL }}/api/users?profileImage=false'
width='25'
style='vertical-align: middle;' />
</span>
<span id='user{{ $row['id'] }}'></span>
@endif
</span>
</a>
<ul class="dropdown-menu"
aria-labelledby="userDropdownMenuLink{{ $row['id'] }}">
<li class="nav-header border">
{{ __('dropdown.choose_user') }}</li>
@foreach ($users as $user)
<li class='dropdown-item'>
<a href='javascript:void(0);'
data-label='{{ sprintf(__('text.full_name'), $user['firstname'], $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'), $user['firstname'], $user['lastname']) }}
</a>
</li>
@endforeach
</ul>
</div>
<div class="right" style="margin-right:10px;">
<a href="#/goalcanvas/editCanvasComment/{{ $row['id'] }}"
class="commentCountLink"
data="item_{{ $row['id'] }}"><span
class="fas fa-comments"></span></a>
<small>{{ $nbcomments }}</small>
</div>
</div>
</div>
@include('goalcanvas::partials.milestoneChips', ['milestones' => $row['milestones'] ?? []])
</div>
</div>
@endif
@endforeach
</div>
<br />
</div>
</div>
</div>
@if (count($canvasItems) == 0)
<br /><br />
<div class='center'>
<div class='svgContainer'>
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_design_data_khdb.svg') !!}
</div>
<h3>{{ __('headlines.goal.analysis') }}</h3>
<br />{!! __('text.goal.helper_content') !!}
</div>
@endif
<div class="clearfix"></div>
@endif
<!-- ShowBottomCanvs -->
@if (count($allCanvas) > 0)
@else
<br /><br />
<div class='center'>
<div class='svgContainer'>
{!! file_get_contents(ROOT . '/dist/images/svg/undraw_design_data_khdb.svg') !!}
</div>
<h3>{{ __('headlines.goal.analysis') }}</h3>
<br />{{ __('text.goal.helper_content') }}
@if ($login::userIsAtLeast($roles::$editor))
<br /><br />
<x-global::forms.button tag="a" link="javascript:void(0)" class="addCanvasLink" contentRole="primary">
{{ __('links.icon.create_new_board') }}
</x-global::forms.button>
@endif
</div>
@endif
@if (!empty($disclaimer) && count($allCanvas) > 0)
<small class="align-center">{{ $disclaimer }}</small>
@endif
{!! $tpl->viewFactory->make($tpl->getTemplatePath('canvas', 'modals'), $__data)->render() !!}
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function() {
if (jQuery('#searchCanvas').length > 0) {
new SlimSelect({
select: '#searchCanvas'
});
}
leantime.goalCanvasController.setRowHeights();
leantime.canvasController.setCanvasName('goal');
leantime.canvasController.initFilterBar();
@if ($login::userIsAtLeast($roles::$editor))
leantime.canvasController.initCanvasLinks();
leantime.canvasController.initUserDropdown();
leantime.canvasController.initStatusDropdown();
leantime.canvasController.initRelatesDropdown();
@else
leantime.authController.makeInputReadonly(".maincontentinner");
@endif
@if (isset($_GET['showModal']))
@php
if ($_GET['showModal'] == '') {
$modalUrl = '&type=' . array_key_first($canvasTypes);
} else {
$modalUrl = '/' . (int) $_GET['showModal'];
}
@endphp
leantime.canvasController.openModalManually(
"{{ BASE_URL }}/goalcanvas/editCanvasItem{{ $modalUrl }}");
window.history.pushState({}, document.title,
'{{ BASE_URL }}/goalcanvas/showCanvas/');
@endif
});
</script>
@endsection