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,104 @@
{{--
Stakeholder Report Page 4 (Programs & Narrative)
§5 page 4: Two columns program rows with RAG + completed count on the
left, status narrative from statusUpdates on the right. "Also this period"
(secondary closures) at the bottom.
Vars in:
$scope 'strategy' | 'program'
$programRows object[] (strategy scope; empty at program scope)
$programUpdates array<int,object[]> (byProject at strategy scope)
--}}
<div class="p4-two">
{{-- ── Programs column (strategy scope only) ─────────────────── --}}
<div>
<div class="p4-lbl">{{ $scope === 'strategy' ? __('stakeholder.programs.label_programs') : __('stakeholder.programs.label_child_projects') }}</div>
@if (count($programRows) === 0)
<div class="p4-empty">{{ $scope === 'strategy' ? __('stakeholder.programs.none') : __('stakeholder.programs.none_projects') }}</div>
@else
@foreach ($programRows as $row)
@php
$row = (array) $row;
// Status → dot color. programRows carries a status field from Marcel's
// buildProgramRows (worst-of-children rollup); values: green/yellow/red/null.
$status = (string) ($row['status'] ?? '');
$dotColor = match ($status) {
'green' => 'var(--rd-ok)',
'yellow' => 'var(--rd-warn)',
'red' => 'var(--rd-danger)',
default => 'var(--rd-text-4)',
};
$statusLabel = match ($status) {
'green' => __('stakeholder.programs.status_ontrack'),
'yellow' => __('stakeholder.programs.status_atrisk'),
'red' => __('stakeholder.programs.status_off'),
default => __('stakeholder.programs.status_none'),
};
$completedCt = (int) ($row['completedCount'] ?? 0);
@endphp
<div class="p4-prog">
<span class="pd" style="background:{{ $dotColor }}"></span>
<span class="pn">{{ $row['name'] ?? '' }}</span>
<span class="pm">{{ $statusLabel }} · {{ sprintf(__('stakeholder.programs.done_count'), $completedCt) }}</span>
</div>
@endforeach
@endif
</div>
{{-- ── Status narrative column ───────────────────────────────── --}}
<div>
<div class="p4-lbl">{{ __('stakeholder.programs.label_narrative') }}</div>
@php
// programUpdates is keyed by projectId. Flatten with a name lookup from
// programRows for the bold label per line.
$namesByProject = [];
foreach ($programRows as $row) {
$row = (array) $row;
$namesByProject[(int) ($row['id'] ?? 0)] = $row['name'] ?? '';
}
$flatUpdates = [];
foreach ($programUpdates as $pid => $updates) {
foreach ($updates as $update) {
$update = (object) $update;
$update->_projectName = $namesByProject[(int) $pid] ?? '';
$flatUpdates[] = $update;
}
}
// Sort newest first — Marcel returns in the same order per project;
// for cross-project flat display we re-sort by date desc.
usort($flatUpdates, fn ($a, $b) => strcmp((string) ($b->date ?? ''), (string) ($a->date ?? '')));
$flatUpdates = array_slice($flatUpdates, 0, 5); // top 5 for the packet view
@endphp
@if (count($flatUpdates) === 0)
<div class="p4-empty">{{ __('stakeholder.programs.no_updates') }}</div>
@else
@foreach ($flatUpdates as $u)
@php
$date = ! empty($u->dateParsed) ? $u->dateParsed->setToUserTimezone()->format('M j') : '';
$text = trim(strip_tags((string) ($u->text ?? '')));
// Truncate long updates — board views want the executive summary.
if (mb_strlen($text) > 220) $text = mb_substr($text, 0, 217).'…';
@endphp
<div class="p4-exec">
@if (! empty($u->_projectName))
<b>{{ $u->_projectName }}</b>
@endif
{{ $text }}
@if ($date !== '') <span class="ed">{{ $date }}</span> @endif
</div>
@endforeach
@endif
</div>
</div>
{{-- "Also this period" secondary closures beyond the peak-this-period hero.
Nomination surface for closures that could be attached to an outcome.
Requires the nomination pass that also feeds the p1 hero; render coming-soon
until that lands, to avoid pretending an empty state is a full one. --}}
<div class="p4-also">
<span class="al-lb">{{ __('stakeholder.programs.also_label') }}</span>
<span style="color:var(--rd-text-3);"> {{ __('stakeholder.programs.also_coming') }}</span>
</div>