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,192 @@
{{--
Renders a single capacity vs. demand card. Used at two levels:
- top-level: one card per program (strategy scope) or per project (program scope)
- nested: inside an expanded program card, one compact card per child project
Vars in:
$c The capacity analysis row (project or program-rolled).
$verdictLabels Map of verdict key translated label.
$compactOnly When true, always render the one-liner regardless of verdict.
--}}
@php
$compactOnly = $compactOnly ?? false;
$vLabel = $verdictLabels[$c['verdict']] ?? $c['verdict'];
$showFull = ! $compactOnly && in_array($c['verdict'], ['critical', 'tight', 'no_capacity'], true);
@endphp
@if ($showFull)
@php
// Balance bar geometry — carry the same reading the text states.
// Fill 0 → available in the "supply" (green) segment.
// Then a distinct "deficit" segment from available → needed, in
// the verdict color. Marker sits AT the available position so a
// reader instantly sees "we have this much, we need this much".
$barMax = max($c['availableHours'], $c['referenceDemand'], 1);
$availableMark = min(100, ($c['availableHours'] / $barMax) * 100);
$demandWidth = min(100, ($c['referenceDemand'] / $barMax) * 100);
$deficitWidth = max(0, $demandWidth - $availableMark);
$gapHrs = abs($c['gap']);
$gapPct = $c['availableHours'] > 0 ? abs($c['gap'] / $c['availableHours']) * 100 : 0;
$isShort = $c['gap'] > 0;
// Run-rate framing. Supply is a current weekly rate; projecting it over
// a (past) period to a total is the fiction we avoid. Demand is a real
// total of estimated work — expressed here as the weekly rate needed to
// clear it within the period. The gap % (verdict) is unchanged: rate
// ratios equal total ratios, so the bar geometry above still holds.
$weeks = max(1, (int) $c['weeksInWindow']);
$supplyPerWk = $c['weeklyHoursToProject'];
$demandPerWk = $c['referenceDemand'] / $weeks;
$gapPerWk = $gapHrs / $weeks;
@endphp
<div class="p3-cap {{ $c['verdict'] }}">
<div class="p3-cap-hd">
<span class="verdict {{ $c['verdict'] }}">
<i class="fa fa-{{ $c['verdict'] === 'critical' ? 'triangle-exclamation' : ($c['verdict'] === 'no_capacity' ? 'ban' : 'circle-exclamation') }}"></i>
{{ $vLabel }}
</span>
<div class="name">{{ $c['name'] }}</div>
@if ($c['trustSignal'] === 'budgeted' && $c['effortHours'] > 0)
<span class="trust good" data-tippy-content="{{ __('stakeholder.rc.cap.trust_budgeted') }}">
<i class="fa fa-check"></i>{{ __('stakeholder.rc.cap.trust_high') }}
</span>
@elseif ($c['trustSignal'] === 'effort')
<span class="trust warn" data-tippy-content="{{ __('stakeholder.rc.cap.trust_effort') }}">
<i class="fa fa-triangle-exclamation"></i>{{ __('stakeholder.rc.cap.trust_effort_short') }}
</span>
@elseif ($c['trustSignal'] === 'mixed')
<span class="trust warn" data-tippy-content="{{ sprintf(__('stakeholder.rc.cap.trust_mixed'), (int) ($c['divergence'] * 100)) }}">
<i class="fa fa-triangle-exclamation"></i>{{ __('stakeholder.rc.cap.trust_mixed_short') }}
</span>
@endif
@if ($c['referenceDemand'] > 0 && $c['availableHours'] > 0)
<div class="headline-num {{ $c['verdict'] }}">
@if ($isShort)
<span class="unit-h" data-hours="{{ round($gapPerWk) }}">{{ round($gapPerWk) }}h</span>/wk {{ sprintf(__('stakeholder.rc.cap.short_suffix'), (int) $gapPct) }}
@else
<span class="unit-h" data-hours="{{ round($gapPerWk) }}">{{ round($gapPerWk) }}h</span>/wk {{ __('stakeholder.rc.cap.buffer_suffix') }}
@endif
</div>
@endif
</div>
<div class="p3-cap-body">
<div class="p3-cap-row">
<div class="lbl">{{ __('stakeholder.rc.cap.scope') }}</div>
<div class="val">
@if ($c['openTicketCount'] === 0)
<span class="muted">{{ __('stakeholder.rc.cap.no_tickets') }}</span>
@else
@if ($c['budgetedHours'] > 0)
<span class="primary"><span class="unit-h" data-hours="{{ round($c['budgetedHours']) }}">{{ round($c['budgetedHours']) }}h</span> {{ __('stakeholder.rc.cap.budgeted') }}</span>
<span class="muted">
({{ $c['ticketsWithBudget'] }}/{{ $c['openTicketCount'] }}
{{ __('stakeholder.rc.cap.tickets_with_hours') }} {{ (int) ($c['coverage'] * 100) }}% {{ __('stakeholder.rc.cap.coverage') }})
</span>
@else
<span class="muted">{{ __('stakeholder.rc.cap.no_budgeted') }} ({{ $c['openTicketCount'] }} {{ __('stakeholder.rc.cap.open_tickets') }})</span>
@endif
<br>
@if ($c['effortPoints'] > 0)
<span class="primary"><span class="unit-h" data-hours="{{ round($c['effortHours']) }}">{{ round($c['effortHours']) }}h</span> {{ __('stakeholder.rc.cap.effort') }}</span>
<span class="muted">({{ $c['effortPoints'] }}
<span class="pts-info" data-tippy-content="{{ __('stakeholder.rc.cap.points_help') }}">pts <i class="fa fa-circle-info"></i></span>
× {{ $c['hoursPerPoint'] }}h/pt)</span>
@else
<span class="muted">{{ __('stakeholder.rc.cap.no_effort') }}</span>
@endif
@endif
</div>
</div>
<div class="p3-cap-row">
<div class="lbl">{{ __('stakeholder.rc.cap.capacity') }}</div>
<div class="val">
@if ($c['peopleCount'] === 0)
<span class="muted">{{ __('stakeholder.rc.cap.no_people') }}</span>
@else
<span class="primary"><span class="unit-h" data-hours="{{ round($supplyPerWk) }}">{{ round($supplyPerWk) }}h</span>/wk {{ __('stakeholder.rc.cap.supply') }}</span>
<span class="muted">
({{ $c['peopleCount'] }} {{ __($c['peopleCount'] === 1 ? 'stakeholder.rc.cap.person' : 'stakeholder.rc.cap.people') }} × <span class="unit-h" data-hours="{{ round($supplyPerWk / max(1, $c['peopleCount']), 1) }}">{{ round($supplyPerWk / max(1, $c['peopleCount']), 1) }}h</span>/wk)
</span>
@endif
</div>
</div>
@if ($c['referenceDemand'] > 0 && $c['availableHours'] > 0)
<div class="p3-cap-row">
<div class="lbl">{{ __('stakeholder.rc.cap.balance') }}</div>
<div class="val">
<span class="unit-h" data-hours="{{ round($demandPerWk) }}">{{ round($demandPerWk) }}h</span>/wk {{ __('stakeholder.rc.cap.demand') }}
<span class="divider">·</span>
<span class="unit-h" data-hours="{{ round($supplyPerWk) }}">{{ round($supplyPerWk) }}h</span>/wk {{ __('stakeholder.rc.cap.supply') }}
<div class="p3-cap-bar">
<div class="track {{ $c['verdict'] }}">
{{-- Supply segment: green fill 0 available. --}}
<div class="supply" style="width:{{ $availableMark }}%;"></div>
{{-- Deficit segment: verdict-color band from
available needed, showing the shortfall. --}}
@if ($isShort && $deficitWidth > 0)
<div class="deficit" style="left:{{ $availableMark }}%;width:{{ $deficitWidth }}%;"
data-tippy-content="{{ sprintf(__("stakeholder.rc.cap.deficit_tooltip"), round($gapPerWk)) }}"></div>
@endif
<div class="marker" style="left:{{ $availableMark }}%;">
<span class="marker-label">{{ __('stakeholder.rc.cap.marker_label') }}</span>
</div>
</div>
<div class="legend">
<span><span class="unit-h" data-hours="0">0h</span></span>
<span><span class="unit-h" data-hours="{{ round($barMax) }}">{{ round($barMax) }}h</span></span>
</div>
</div>
</div>
</div>
@endif
@if ($isShort && $c['recommendations']['extendWeeks'] > 0)
@php $r = $c['recommendations']; @endphp
<div class="p3-cap-rebalance">
<div class="hd">{{ __('stakeholder.rc.cap.rebalance_hd') }}</div>
<div class="opts">
<div class="opt">
<div class="icn"><i class="fa fa-calendar-plus"></i></div>
<div class="lever">{{ __('stakeholder.rc.cap.lever_extend_pre') }} <b>{{ $r['extendWeeks'] }}</b> {{ __('stakeholder.rc.cap.lever_extend_post') }}</div>
<div class="detail">{{ sprintf(__('stakeholder.rc.cap.lever_extend_detail'), round($c['weeklyHoursToProject'])) }}</div>
</div>
<div class="opt">
<div class="icn"><i class="fa fa-user-plus"></i></div>
<div class="lever">{{ __('stakeholder.rc.cap.lever_add_pre') }} <b>{{ $r['addPeople'] }}</b> {{ $r['addPeople'] === 1 ? __('stakeholder.rc.cap.lever_add_post_one') : __('stakeholder.rc.cap.lever_add_post_many') }}</div>
<div class="detail">{{ sprintf(__('stakeholder.rc.cap.lever_add_detail'), round($c['weeklyHoursToProject'] / max(1, $c['peopleCount']), 1), $c['weeksInWindow']) }}</div>
</div>
<div class="opt">
<div class="icn"><i class="fa fa-scissors"></i></div>
<div class="lever">{{ __('stakeholder.rc.cap.lever_cut_pre') }} <b>{{ round($r['cutPoints']) }}</b> {{ __('stakeholder.rc.cap.lever_cut_post') }}</div>
<div class="detail">{{ sprintf(__('stakeholder.rc.cap.lever_cut_detail'), round($r['cutHours']), $c['hoursPerPoint']) }}</div>
</div>
</div>
</div>
@endif
</div>
</div>
@else
{{-- Compact one-liner --}}
<div class="p3-cap-compact">
<span class="verdict {{ $c['verdict'] }}">
@if ($c['verdict'] === 'buffer')<i class="fa fa-check"></i>@else<i class="fa fa-minus"></i>@endif
{{ $vLabel }}
</span>
<div class="name">{{ $c['name'] }}</div>
<div class="summary">
@if ($c['verdict'] === 'no_work')
{{ __('stakeholder.rc.cap.summary_no_work') }}
@else
<span class="unit-h" data-hours="{{ round($c['referenceDemand']) }}">{{ round($c['referenceDemand']) }}h</span> {{ __('stakeholder.rc.cap.summary_needed') }}
·
<span class="unit-h" data-hours="{{ round($c['availableHours']) }}">{{ round($c['availableHours']) }}h</span> {{ __('stakeholder.rc.cap.summary_available') }}
@endif
</div>
</div>
@endif