refactor(archive): redesign segmented archive experience

This commit is contained in:
2026-04-14 15:09:25 +02:00
parent 297f63c7d5
commit ab1d8bc677
6 changed files with 1108 additions and 232 deletions
+559
View File
@@ -48,6 +48,30 @@
--control-soft-bg: rgba(255, 255, 255, 0.08);
--control-soft-border: rgba(255, 255, 255, 0.16);
--brand-shadow: 0 10px 24px rgba(9, 25, 40, 0.22);
--archive-shell-bg:
linear-gradient(180deg, rgba(22, 38, 58, 0.94), rgba(10, 25, 41, 0.92)),
radial-gradient(circle at top right, rgba(59, 173, 212, 0.12), transparent 42%);
--archive-shell-border: rgba(148, 198, 228, 0.18);
--archive-toolbar-bg:
linear-gradient(180deg, rgba(34, 57, 79, 0.82), rgba(22, 40, 58, 0.76)),
radial-gradient(circle at top right, rgba(75, 203, 223, 0.1), transparent 48%);
--archive-toolbar-border: rgba(148, 198, 228, 0.14);
--archive-switcher-bg: rgba(12, 24, 38, 0.34);
--archive-switcher-border: rgba(148, 198, 228, 0.12);
--archive-switcher-active-bg: rgba(173, 213, 245, 0.14);
--archive-row-bg: rgba(255, 255, 255, 0.06);
--archive-row-border: rgba(255, 255, 255, 0.05);
--archive-row-active-bg: rgba(255, 255, 255, 0.09);
--archive-detail-bg:
linear-gradient(180deg, rgba(40, 62, 86, 0.88), rgba(24, 41, 60, 0.82)),
radial-gradient(circle at top left, rgba(135, 217, 255, 0.12), transparent 42%);
--archive-select-bg: rgba(30, 51, 72, 0.84);
--archive-select-border: rgba(148, 198, 228, 0.18);
--archive-select-focus-bg: rgba(35, 59, 83, 0.94);
--archive-mobile-overlay-bg: rgba(6, 14, 24, 0.36);
--archive-mobile-top-bg:
linear-gradient(180deg, rgba(17, 33, 50, 0.96), rgba(17, 33, 50, 0.78)),
radial-gradient(circle at top left, rgba(135, 217, 255, 0.1), transparent 42%);
}
@media (prefers-color-scheme: light) {
@@ -95,6 +119,30 @@
--control-soft-bg: rgba(255, 255, 255, 0.58);
--control-soft-border: rgba(123, 153, 182, 0.22);
--brand-shadow: 0 10px 24px rgba(82, 111, 138, 0.14);
--archive-shell-bg:
linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(247, 252, 255, 0.72)),
radial-gradient(circle at top right, rgba(123, 190, 255, 0.16), transparent 46%);
--archive-shell-border: rgba(120, 146, 172, 0.2);
--archive-toolbar-bg:
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(245, 251, 255, 0.64)),
radial-gradient(circle at top right, rgba(106, 203, 219, 0.12), transparent 48%);
--archive-toolbar-border: rgba(120, 146, 172, 0.16);
--archive-switcher-bg: rgba(255, 255, 255, 0.34);
--archive-switcher-border: rgba(120, 146, 172, 0.18);
--archive-switcher-active-bg: rgba(255, 255, 255, 0.72);
--archive-row-bg: rgba(255, 255, 255, 0.38);
--archive-row-border: rgba(120, 146, 172, 0.14);
--archive-row-active-bg: rgba(255, 255, 255, 0.56);
--archive-detail-bg:
linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(244, 250, 255, 0.72)),
radial-gradient(circle at top left, rgba(141, 205, 255, 0.16), transparent 42%);
--archive-select-bg: rgba(255, 255, 255, 0.62);
--archive-select-border: rgba(123, 153, 182, 0.2);
--archive-select-focus-bg: rgba(255, 255, 255, 0.84);
--archive-mobile-overlay-bg: rgba(236, 243, 249, 0.42);
--archive-mobile-top-bg:
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(246, 251, 255, 0.84)),
radial-gradient(circle at top left, rgba(141, 205, 255, 0.16), transparent 42%);
}
html {
@@ -104,6 +152,31 @@
select {
color-scheme: light;
}
.mobile-nav .nav-icon {
opacity: 1;
filter:
saturate(2.1)
contrast(1.16)
brightness(0.76)
drop-shadow(0 1px 0 rgba(255, 255, 255, 0.5))
drop-shadow(0 0 8px rgba(110, 214, 255, 0.18));
}
.mobile-nav a.active .nav-icon {
filter:
saturate(2.35)
contrast(1.2)
brightness(0.68)
drop-shadow(0 1px 0 rgba(255, 255, 255, 0.56))
drop-shadow(0 0 10px rgba(110, 214, 255, 0.24));
}
.detail-grid--archive-day div {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(244, 249, 255, 0.64));
border: 1px solid rgba(126, 156, 184, 0.18);
box-shadow: 0 8px 22px rgba(138, 167, 194, 0.1);
}
}
*,
@@ -851,6 +924,10 @@ button:disabled {
gap: 0.7rem;
}
.sport-choice-list--single {
grid-template-columns: minmax(0, 1fr);
}
.sport-choice {
display: block;
}
@@ -890,6 +967,11 @@ button:disabled {
line-height: 1.3;
}
.sport-choice__card--toggle {
min-height: 100%;
align-content: center;
}
.sport-choice input:checked + .sport-choice__card {
border-color: rgba(139, 228, 255, 0.44);
background: linear-gradient(180deg, rgba(96, 184, 255, 0.18), rgba(255, 255, 255, 0.08));
@@ -1156,6 +1238,399 @@ input[type="range"] {
margin-bottom: 1.2rem;
}
.archive-page {
display: grid;
}
.archive-shell {
display: grid;
gap: 1rem;
padding: 1.2rem;
border-radius: var(--radius-xl);
overflow: hidden;
background: var(--archive-shell-bg);
border-color: var(--archive-shell-border);
}
.archive-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.archive-header__meta {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
justify-content: flex-end;
}
.archive-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
padding: 0.85rem 0.95rem;
border-radius: 18px;
background: var(--archive-toolbar-bg);
border: 1px solid var(--archive-toolbar-border);
}
.archive-toolbar--compact {
margin-bottom: 0.15rem;
}
.archive-switcher {
display: inline-grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.35rem;
padding: 0.25rem;
border-radius: 999px;
background: var(--archive-switcher-bg);
border: 1px solid var(--archive-switcher-border);
}
.archive-switcher__item {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.45rem;
padding: 0.45rem 0.9rem;
border-radius: 999px;
color: var(--muted);
transition: background 180ms ease, color 180ms ease, transform 180ms ease;
}
.archive-switcher__item.active {
color: var(--text);
background: var(--archive-switcher-active-bg);
transform: translateY(-1px);
}
.archive-filter {
display: flex;
align-items: end;
gap: 0.8rem;
}
.archive-filter label {
min-width: 13rem;
}
.archive-filter select {
background: var(--archive-select-bg);
border-color: var(--archive-select-border);
}
.archive-filter select:focus {
background: var(--archive-select-focus-bg);
}
.archive-workspace {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.78fr);
gap: 1rem;
align-items: start;
}
.archive-main {
display: grid;
gap: 0.85rem;
min-width: 0;
}
.archive-list-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.archive-rows {
display: grid;
gap: 0.7rem;
}
.archive-row {
display: grid;
grid-template-columns: minmax(0, 1.2fr) auto auto;
align-items: center;
gap: 0.85rem;
padding: 0.95rem 1rem;
border-radius: 18px;
background: var(--archive-row-bg);
border: 1px solid var(--archive-row-border);
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
}
.archive-row:hover,
.archive-row.active {
transform: translateY(-1px);
border-color: rgba(139, 228, 255, 0.28);
background: var(--archive-row-active-bg);
}
.archive-row__main,
.archive-row__meta {
display: grid;
gap: 0.18rem;
min-width: 0;
}
.archive-row__main strong,
.archive-row__title-group strong {
font-size: 1rem;
line-height: 1.25;
}
.archive-row__main span,
.archive-row__meta span,
.archive-row__title-group span,
.archive-row__hint {
color: var(--muted);
font-size: 0.92rem;
overflow-wrap: normal;
word-break: normal;
hyphens: auto;
}
.archive-row__meta {
justify-items: end;
text-align: right;
}
.archive-row__meta--stack {
justify-items: start;
text-align: left;
}
.archive-row__hint {
white-space: nowrap;
}
.archive-row__title-group span,
.archive-row__meta span {
line-height: 1.45;
}
.archive-row--summary {
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.95fr) auto;
align-items: flex-start;
}
.archive-row--summary .archive-row__main {
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
}
.archive-row--summary .status-badge {
justify-self: end;
align-self: start;
}
.archive-row--week .archive-row__main--week {
grid-template-columns: 1fr;
gap: 0.6rem;
}
.archive-row--week .archive-row__main--week .status-badge,
.archive-row--month .archive-row__main--month .status-badge {
justify-self: start;
}
.archive-row--month .archive-row__main--month {
grid-template-columns: 1fr;
gap: 0.6rem;
}
.archive-row--summary .archive-row__hint {
align-self: start;
}
.archive-row__title-group {
display: grid;
gap: 0.18rem;
min-width: 0;
}
.status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.7rem;
padding: 0.2rem 0.65rem;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.03em;
border: 1px solid transparent;
max-width: 100%;
white-space: normal;
line-height: 1.25;
text-align: center;
}
.status-badge--ready {
color: #082336;
background: rgba(127, 243, 187, 0.92);
}
.status-badge--pending {
color: var(--text);
background: rgba(139, 228, 255, 0.14);
border-color: rgba(139, 228, 255, 0.22);
}
.status-badge--blocked {
color: var(--text);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.12);
}
.archive-detail {
min-width: 0;
}
.archive-detail__panel {
display: grid;
gap: 1rem;
padding: 1.15rem;
border-radius: var(--radius-xl);
position: sticky;
top: 1.25rem;
min-width: 0;
overflow: hidden;
background: var(--archive-detail-bg);
border-color: var(--archive-shell-border);
}
.archive-detail__top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
min-width: 0;
}
.archive-detail__close {
display: none;
}
.archive-detail__status-row,
.archive-detail__actions {
display: flex;
align-items: center;
gap: 0.7rem;
flex-wrap: wrap;
min-width: 0;
}
.archive-detail__single-action form,
.archive-detail__actions form {
margin: 0;
min-width: 0;
}
.archive-detail__single-action {
display: flex;
min-width: 0;
}
.archive-detail__week-status {
display: grid;
gap: 0.7rem;
}
.archive-detail__status-note {
display: grid;
gap: 0.4rem;
}
.archive-detail__status-note p {
margin: 0;
}
.archive-mini-list {
display: grid;
gap: 0.55rem;
}
.archive-mini-list__row {
display: flex;
justify-content: space-between;
gap: 0.7rem;
align-items: center;
padding: 0.72rem 0.85rem;
border-radius: 14px;
background: rgba(255, 255, 255, 0.07);
min-width: 0;
}
.archive-detail__panel > *,
.archive-detail__top > *,
.archive-detail__status-row > *,
.archive-detail__actions > * {
min-width: 0;
}
.archive-detail__actions .ghost-link,
.archive-detail__actions .ghost-button,
.archive-detail__single-action .primary-button {
max-width: 100%;
white-space: normal;
}
.archive-detail__status-row .chart-chip {
max-width: 100%;
white-space: normal;
text-align: center;
}
.archive-detail__single-action .primary-button {
width: 100%;
}
.detail-grid--archive div {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
align-items: flex-start;
min-width: 0;
}
.detail-grid--archive-day {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.8rem;
}
.detail-grid--archive-day div {
display: grid;
gap: 0.28rem;
align-content: start;
}
.detail-grid--archive-day dt,
.detail-grid--archive-day dd {
min-width: 0;
}
.detail-grid--archive-day dd {
text-align: left;
white-space: normal;
}
.detail-grid--archive dd,
.archive-mini-list__row span,
.note-box p {
min-width: 0;
overflow-wrap: anywhere;
}
.detail-grid--archive dd {
text-align: right;
}
.archive-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
@@ -1606,6 +2081,75 @@ input[type="range"] {
grid-template-columns: 1fr;
}
.archive-header,
.archive-toolbar,
.archive-workspace,
.archive-list-header,
.archive-row,
.archive-row--summary {
grid-template-columns: 1fr;
}
.archive-header,
.archive-toolbar,
.archive-list-header {
flex-direction: column;
align-items: stretch;
}
.archive-workspace {
gap: 0.85rem;
}
.archive-row__meta {
justify-items: start;
text-align: left;
}
.archive-row--summary .archive-row__main {
grid-template-columns: 1fr;
}
.archive-row--summary .status-badge {
justify-self: start;
}
.archive-row--day {
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 0.75rem;
padding: 0.9rem 0.95rem;
}
.archive-row--day .archive-row__main {
gap: 0.12rem;
}
.archive-row--day .archive-row__meta {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
gap: 0.12rem;
}
.archive-row--day .archive-row__hint {
display: none;
}
.archive-row--day .archive-row__main strong {
font-size: 1.02rem;
}
.archive-row--day .archive-row__main span,
.archive-row--day .archive-row__meta span {
font-size: 0.9rem;
}
.archive-detail {
scroll-margin-top: 1rem;
}
.calendar-detail {
flex-direction: column;
align-items: flex-start;
@@ -1680,4 +2224,19 @@ input[type="range"] {
align-items: flex-start;
gap: 0.4rem;
}
.archive-shell {
padding: 1rem;
}
.archive-switcher,
.archive-filter,
.archive-header__meta {
width: 100%;
}
.archive-filter label {
min-width: 0;
}
}
+27 -1
View File
@@ -70,6 +70,25 @@
});
}
function initArchiveMobileDetail() {
if (!document.body.classList.contains("page-archive")) {
return;
}
const isMobileViewport = () => window.matchMedia("(max-width: 820px)").matches;
const detail = document.querySelector("#archive-detail-panel[data-detail-open='1']");
if (!detail || !isMobileViewport()) {
return;
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
detail.scrollIntoView({ block: "start", behavior: "smooth" });
});
});
}
function sleepDurationPoints(hours, points) {
if (hours < 4) {
return Number(points.lt4 || 0);
@@ -542,8 +561,12 @@
}
function calendarColor(entry) {
const isLightMode = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches;
if (!entry) {
return "rgba(255, 255, 255, 0.06)";
return isLightMode
? "rgba(86, 124, 156, 0.11)"
: "rgba(255, 255, 255, 0.06)";
}
const ratio = Math.max(0, Math.min(1, Number(entry.score) / Math.max(Number(entry.max || 1), 1)));
@@ -644,6 +667,7 @@
const yOffset = config.yOffset;
const gridHeight = (7 * cellSize) + (6 * verticalGap);
const height = yOffset + gridHeight + 8;
const sundayLabelY = yOffset + (6 * (cellSize + verticalGap)) + (cellSize * 0.78);
const rightPadding = 4;
const naturalWidth = xOffset + (totalWeeks * cellSize) + ((totalWeeks - 1) * baseCellGap) + rightPadding;
const availableWidth = Math.floor(container.clientWidth || 0);
@@ -718,6 +742,7 @@
<text class="calendar-tooltip" x="0" y="34">Mo</text>
<text class="calendar-tooltip" x="0" y="68">Mi</text>
<text class="calendar-tooltip" x="0" y="102">Fr</text>
<text class="calendar-tooltip" x="0" y="${sundayLabelY}">So</text>
${cells}
</svg>
<div class="calendar-legend">
@@ -1273,6 +1298,7 @@
updateRangeOutputs();
initHeaderDatePicker();
initTrackPreview();
initArchiveMobileDetail();
initDashboardCharts();
initSportTypeManager();
initPwaShell();
+222 -21
View File
@@ -345,17 +345,47 @@ final class App
{
$user = $this->requireUser();
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
$view = $this->normalizeArchiveView((string) ($_GET['view'] ?? 'days'));
$filterMonth = trim((string) ($_GET['filter_month'] ?? ''));
if ($filterMonth !== '' && preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $filterMonth) !== 1) {
$filterMonth = '';
}
$selectedDate = isset($_GET['date']) ? (string) $_GET['date'] : null;
$selectedSummaryKind = isset($_GET['summary_kind']) ? (string) $_GET['summary_kind'] : null;
$selectedSummaryKey = isset($_GET['summary_key']) ? (string) $_GET['summary_key'] : null;
$selectedWeekKey = isset($_GET['week'] ) ? trim((string) $_GET['week']) : null;
$selectedMonthKey = isset($_GET['month_key']) ? trim((string) $_GET['month_key']) : null;
$entries = $this->entries->all($user['username']);
$archive = array_reverse($this->evaluateEntriesWithContext($entries, $settings));
$weeklySummaries = $this->summaries->weekly($user['username']);
$monthlySummaries = $this->summaries->monthly($user['username']);
$weeklyArchive = $this->buildWeeklyArchiveCards($archive, $weeklySummaries);
$monthlyArchive = $this->buildMonthlyArchiveCards($archive, $weeklySummaries, $monthlySummaries, $weeklyArchive);
$monthOptions = $this->buildArchiveMonthOptions($archive, $weeklyArchive, $monthlyArchive);
$filteredDays = $filterMonth === ''
? $archive
: array_values(array_filter(
$archive,
static fn (array $entry): bool => month_key((string) ($entry['date'] ?? '')) === $filterMonth
));
$filteredWeeks = $filterMonth === ''
? $weeklyArchive
: array_values(array_filter(
$weeklyArchive,
fn (array $week): bool => $this->archiveItemOverlapsMonth($week, $filterMonth)
));
$filteredMonths = $filterMonth === ''
? $monthlyArchive
: array_values(array_filter(
$monthlyArchive,
static fn (array $month): bool => (string) ($month['summary_key'] ?? '') === $filterMonth
));
$selectedEntry = null;
if ($selectedDate !== null) {
foreach ($archive as $entry) {
if ($view === 'days' && $selectedDate !== null) {
foreach ($filteredDays as $entry) {
if ($entry['date'] === $selectedDate) {
$selectedEntry = $entry;
break;
@@ -363,21 +393,40 @@ final class App
}
}
$selectedSummary = null;
if ($selectedSummaryKind !== null && $selectedSummaryKey !== null) {
$selectedSummary = $this->summaries->find($user['username'], $selectedSummaryKind, $selectedSummaryKey);
$selectedWeek = null;
if ($view === 'weeks' && $selectedWeekKey !== null) {
foreach ($filteredWeeks as $week) {
if (($week['summary_key'] ?? '') === $selectedWeekKey) {
$selectedWeek = $week;
break;
}
}
}
$selectedMonth = null;
if ($view === 'months' && $selectedMonthKey !== null) {
foreach ($filteredMonths as $month) {
if (($month['summary_key'] ?? '') === $selectedMonthKey) {
$selectedMonth = $month;
break;
}
}
}
View::render('archive', [
'pageTitle' => 'Archiv',
'page' => 'archive',
'authUser' => $user,
'entries' => $archive,
'entries' => $filteredDays,
'selectedEntry' => $selectedEntry,
'selectedSummary' => $selectedSummary,
'selectedWeek' => $selectedWeek,
'selectedMonth' => $selectedMonth,
'settings' => $settings,
'weeklyArchive' => $this->buildWeeklyArchiveCards($archive, $weeklySummaries),
'monthlyArchive' => $this->buildMonthlyArchiveCards($archive, $weeklySummaries, $monthlySummaries),
'archiveView' => $view,
'archiveFilterMonth' => $filterMonth,
'archiveMonthOptions' => $monthOptions,
'weeklyArchive' => $filteredWeeks,
'monthlyArchive' => $filteredMonths,
'aiAvailable' => $this->openAi->isAvailable(),
]);
}
@@ -390,6 +439,11 @@ final class App
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
$entries = $this->evaluateEntriesWithContext($this->entries->all($user['username']), $settings);
$form = (string) ($_POST['form_name'] ?? '');
$returnView = $this->normalizeArchiveView((string) ($_POST['view'] ?? 'days'));
$returnFilterMonth = trim((string) ($_POST['filter_month'] ?? ''));
if ($returnFilterMonth !== '' && preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $returnFilterMonth) !== 1) {
$returnFilterMonth = '';
}
try {
if ($form === 'generate_weekly_summary') {
@@ -406,7 +460,11 @@ final class App
]);
flash('success', 'Die KI-Wochenzusammenfassung wurde erstellt.');
redirect('/archive?summary_kind=weekly&summary_key=' . rawurlencode($weekKey));
redirect($this->archivePath([
'view' => 'weeks',
'filter_month' => $returnFilterMonth !== '' ? $returnFilterMonth : month_key($context['date_to']),
'week' => $weekKey,
]));
}
if ($form === 'generate_monthly_summary') {
@@ -425,14 +483,24 @@ final class App
]);
flash('success', 'Die KI-Monatszusammenfassung wurde erstellt.');
redirect('/archive?summary_kind=monthly&summary_key=' . rawurlencode($monthKey));
redirect($this->archivePath([
'view' => 'months',
'filter_month' => $returnFilterMonth !== '' ? $returnFilterMonth : $monthKey,
'month_key' => $monthKey,
]));
}
} catch (RuntimeException $exception) {
flash('error', $exception->getMessage());
redirect('/archive');
redirect($this->archivePath([
'view' => $returnView,
'filter_month' => $returnFilterMonth,
]));
}
redirect('/archive');
redirect($this->archivePath([
'view' => $returnView,
'filter_month' => $returnFilterMonth,
]));
}
private function showOptions(): void
@@ -1107,6 +1175,21 @@ final class App
$weekEntries,
static fn (array $entry): bool => trim((string) ($entry['note'] ?? '')) !== ''
));
$canGenerate = count($noteEntries) >= 3;
if ($summary !== null) {
$statusLabel = 'KI vorhanden';
$statusTone = 'ready';
$statusHint = 'Erstellt am ' . format_compact_datetime((string) ($summary['created_at'] ?? ''));
} elseif ($canGenerate) {
$statusLabel = 'KI möglich';
$statusTone = 'pending';
$statusHint = 'KI-Wochenzusammenfassung kann erzeugt werden';
} else {
$statusLabel = 'KI nicht möglich';
$statusTone = 'blocked';
$statusHint = 'Mindestens 3 Texteinträge nötig';
}
$cards[] = [
'summary_key' => $key,
@@ -1115,9 +1198,13 @@ final class App
'date_to' => $summary['date_to'] ?? $range['date_to'],
'tracked_days' => count($weekEntries),
'note_entries_count' => count($noteEntries),
'can_generate' => count($noteEntries) >= 3,
'can_generate' => $canGenerate,
'summary' => $summary,
'has_summary' => $summary !== null,
'status_label' => $statusLabel,
'status_tone' => $statusTone,
'status_hint' => $statusHint,
'trend_label' => $this->weeklyTrendLabel($weekEntries),
];
}
@@ -1126,7 +1213,7 @@ final class App
return $cards;
}
private function buildMonthlyArchiveCards(array $entries, array $weeklySummaries, array $monthlySummaries): array
private function buildMonthlyArchiveCards(array $entries, array $weeklySummaries, array $monthlySummaries, array $weeklyArchive): array
{
$monthKeys = [];
@@ -1163,8 +1250,32 @@ final class App
$weeklySummaries,
fn (array $summary): bool => $this->summaryOverlapsMonth($summary, $monthKey)
));
$monthWeeks = array_values(array_filter(
$weeklyArchive,
fn (array $week): bool => $this->archiveItemOverlapsMonth($week, $monthKey)
));
usort($monthWeeklySummaries, static fn (array $left, array $right): int => strcmp((string) ($left['date_from'] ?? ''), (string) ($right['date_from'] ?? '')));
usort($monthWeeks, static fn (array $left, array $right): int => strcmp((string) ($left['date_from'] ?? ''), (string) ($right['date_from'] ?? '')));
$availableWeeklyCount = count($monthWeeklySummaries);
$totalWeekCount = count($monthWeeks);
$canGenerate = $availableWeeklyCount >= 2;
$summary = $monthlySummaryMap[$monthKey] ?? null;
if ($summary !== null) {
$statusLabel = 'KI vorhanden';
$statusTone = 'ready';
$statusHint = 'Erstellt am ' . format_compact_datetime((string) ($summary['created_at'] ?? ''));
} elseif ($canGenerate) {
$statusLabel = 'KI möglich';
$statusTone = 'pending';
$statusHint = 'KI-Monatszusammenfassung kann erzeugt werden';
} else {
$statusLabel = 'KI nicht möglich';
$statusTone = 'blocked';
$statusHint = 'Mindestens 2 KI-Wochenzusammenfassungen nötig';
}
$cards[] = [
'summary_key' => $monthKey,
@@ -1172,10 +1283,22 @@ final class App
'date_from' => $range['date_from'],
'date_to' => $range['date_to'],
'tracked_days' => count($monthEntries),
'weekly_summary_count' => count($monthWeeklySummaries),
'can_generate' => count($monthWeeklySummaries) >= 2,
'summary' => $monthlySummaryMap[$monthKey] ?? null,
'has_summary' => isset($monthlySummaryMap[$monthKey]),
'weekly_summary_count' => $availableWeeklyCount,
'weekly_total_count' => $totalWeekCount,
'weekly_progress_label' => $availableWeeklyCount . ' von ' . ($totalWeekCount > 0 ? $totalWeekCount : 0) . ' Wochen mit KI',
'can_generate' => $canGenerate,
'summary' => $summary,
'has_summary' => $summary !== null,
'status_label' => $statusLabel,
'status_tone' => $statusTone,
'status_hint' => $statusHint,
'weeks' => array_map(static function (array $week): array {
return [
'label' => (string) ($week['label'] ?? ''),
'summary_key' => (string) ($week['summary_key'] ?? ''),
'has_summary' => !empty($week['has_summary']),
];
}, $monthWeeks),
];
}
@@ -1444,6 +1567,84 @@ final class App
&& $summaryTo >= $range['date_from'];
}
private function buildArchiveMonthOptions(array $entries, array $weeklyArchive, array $monthlyArchive): array
{
$keys = [];
foreach ($entries as $entry) {
$keys[month_key((string) ($entry['date'] ?? ''))] = true;
}
foreach ($weeklyArchive as $week) {
foreach ($this->monthKeysForRange((string) ($week['date_from'] ?? ''), (string) ($week['date_to'] ?? '')) as $monthKey) {
$keys[$monthKey] = true;
}
}
foreach ($monthlyArchive as $month) {
$keys[(string) ($month['summary_key'] ?? '')] = true;
}
$options = array_values(array_filter(array_keys($keys), static fn (string $key): bool => $key !== ''));
rsort($options, SORT_STRING);
return $options;
}
private function normalizeArchiveView(string $view): string
{
$view = trim($view);
return in_array($view, ['days', 'weeks', 'months'], true) ? $view : 'days';
}
private function weeklyTrendLabel(array $entries): string
{
if ($entries === []) {
return 'Keine Tendenz';
}
$mood = $this->average($entries, 'mood');
$stress = $this->average($entries, 'stress');
if ($mood >= 7 && $stress <= 4) {
return 'eher stabil';
}
if ($mood <= 4 || $stress >= 7) {
return 'eher belastet';
}
return 'gemischte Tendenz';
}
private function archiveItemOverlapsMonth(array $item, string $monthKey): bool
{
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $monthKey) !== 1) {
return true;
}
$dateFrom = (string) ($item['date_from'] ?? '');
$dateTo = (string) ($item['date_to'] ?? '');
if ($dateFrom === '' || $dateTo === '') {
return false;
}
$range = $this->monthRangeFromKey($monthKey);
return $dateFrom <= $range['date_to'] && $dateTo >= $range['date_from'];
}
private function archivePath(array $params = []): string
{
$filtered = array_filter($params, static fn (mixed $value): bool => $value !== null && $value !== '');
return $filtered === []
? '/archive'
: '/archive?' . http_build_query($filtered);
}
private function sendSecurityHeaders(): void
{
header('Referrer-Policy: strict-origin-when-cross-origin');
+22
View File
@@ -197,6 +197,17 @@ function format_display_date(string $date, bool $withWeekday = true): string
return $weekdays[(int) $current->format('w')] . ', ' . $label;
}
function format_compact_date(string $date): string
{
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
if ($current === false) {
return $date;
}
return $current->format('d.m.Y');
}
function format_display_datetime(string $value): string
{
try {
@@ -223,6 +234,17 @@ function format_display_datetime(string $value): string
return $current->format('j.') . ' ' . $months[(int) $current->format('n')] . ' ' . $current->format('Y') . ' um ' . $current->format('H:i');
}
function format_compact_datetime(string $value): string
{
try {
$current = new DateTimeImmutable($value);
} catch (Throwable) {
return $value;
}
return $current->format('d.m.Y · H:i');
}
function iso_week_key(string $date): string
{
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
+4 -2
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
$brandSubtitle = match ($page) {
'dashboard' => 'Statistiken und Verlauf',
'track' => 'Tag erfassen und bewerten',
'archive' => 'Rückblick auf vergangene Tage',
'archive' => '',
'options' => 'Logik, Erinnerungen, Sicherheit und Accounts',
'login' => 'Geschützter Zugang',
'setup' => 'Erstkonfiguration',
@@ -36,7 +36,7 @@ $brandSubtitle = match ($page) {
<link rel="stylesheet" href="/assets/css/app.css">
<script defer src="/assets/js/app.js"></script>
</head>
<body class="app-body page-<?= e($page) ?><?= $authUser !== null ? ' is-authenticated' : '' ?>" data-authenticated="<?= $authUser !== null ? '1' : '0' ?>"<?= isset($trackMood) ? ' data-track-mood="' . e($trackMood) . '"' : '' ?>>
<body class="app-body page-<?= e($page) ?><?= $authUser !== null ? ' is-authenticated' : '' ?><?= !empty($pageBodyClass) ? ' ' . e((string) $pageBodyClass) : '' ?>" data-authenticated="<?= $authUser !== null ? '1' : '0' ?>"<?= isset($trackMood) ? ' data-track-mood="' . e($trackMood) . '"' : '' ?>>
<div class="aurora aurora-one"></div>
<div class="aurora aurora-two"></div>
<div class="pull-refresh-indicator glass-panel" data-pull-refresh-indicator aria-hidden="true">Zum Aktualisieren ziehen</div>
@@ -89,7 +89,9 @@ $brandSubtitle = match ($page) {
<?php if ($authUser !== null): ?>
<header class="topbar glass-panel">
<div>
<?php if ($brandSubtitle !== ''): ?>
<p class="eyebrow"><?= e($brandSubtitle) ?></p>
<?php endif; ?>
<h2><?= e($pageTitle) ?></h2>
</div>
<div class="topbar__meta">
+250 -184
View File
@@ -1,190 +1,169 @@
<section class="page-grid">
<article class="glass-panel archive-list">
<div class="section-head">
<div>
<p class="eyebrow">Archiv</p>
<h3>KI-Rückblicke und gespeicherte Tage</h3>
</div>
<span class="chart-chip"><?= e((string) count($entries)) ?> Einträge</span>
</div>
<?php
$baseParams = ['view' => $archiveView];
if ($archiveFilterMonth !== '') {
$baseParams['filter_month'] = $archiveFilterMonth;
}
<section class="archive-summary-section">
<div class="section-head section-head--compact">
<div>
<p class="eyebrow">KI</p>
<h4>Monatszusammenfassungen</h4>
</div>
<?php if (empty($aiAvailable)): ?>
<span class="chart-chip chart-chip--muted">API nicht bereit</span>
<?php endif; ?>
</div>
$archiveUrl = static function (array $params = []) use ($baseParams): string {
$query = array_filter(array_merge($baseParams, $params), static fn (mixed $value): bool => $value !== null && $value !== '');
<?php if ($monthlyArchive === []): ?>
<p class="empty-state">Sobald genügend Wochenzusammenfassungen vorliegen, erscheinen hier die Monatsrückblicke.</p>
<?php else: ?>
<div class="archive-summary-grid">
<?php foreach ($monthlyArchive as $month): ?>
<article class="archive-summary-card">
<div class="archive-summary-card__head">
<div>
<span class="summary-badge">KI</span>
<strong><?= e($month['label']) ?></strong>
</div>
<?php if (!empty($month['has_summary'])): ?>
<span class="chart-chip">vorhanden</span>
<?php else: ?>
<span class="chart-chip chart-chip--muted">offen</span>
<?php endif; ?>
</div>
return $query === [] ? '/archive' : '/archive?' . http_build_query($query);
};
<p class="helper-text"><?= e($month['date_from']) ?> bis <?= e($month['date_to']) ?></p>
<p class="helper-text"><?= e((string) $month['weekly_summary_count']) ?> KI-Wochenzusammenfassungen im Monat verfügbar</p>
$detailType = $selectedEntry !== null
? 'day'
: ($selectedWeek !== null
? 'week'
: ($selectedMonth !== null ? 'month' : null));
<?php if (!empty($month['summary'])): ?>
<p class="helper-text">Erstellt am <?= e(format_display_datetime((string) $month['summary']['created_at'])) ?></p>
<?php else: ?>
<p class="helper-text">Mindestens 2 KI-Wochenzusammenfassungen nötig.</p>
<?php endif; ?>
$detailOpen = $detailType !== null;
?>
<div class="archive-item__actions archive-item__actions--stack">
<?php if (!empty($month['summary'])): ?>
<a class="ghost-link archive-action" href="/archive?summary_kind=monthly&amp;summary_key=<?= e(rawurlencode((string) $month['summary_key'])) ?>">Öffnen</a>
<?php endif; ?>
<section class="archive-page">
<article class="glass-panel archive-shell">
<div class="archive-toolbar archive-toolbar--compact">
<nav class="archive-switcher" aria-label="Archivansicht">
<a class="archive-switcher__item <?= $archiveView === 'days' ? 'active' : '' ?>" href="<?= e('/archive?view=days' . ($archiveFilterMonth !== '' ? '&filter_month=' . rawurlencode($archiveFilterMonth) : '')) ?>">Tage</a>
<a class="archive-switcher__item <?= $archiveView === 'weeks' ? 'active' : '' ?>" href="<?= e('/archive?view=weeks' . ($archiveFilterMonth !== '' ? '&filter_month=' . rawurlencode($archiveFilterMonth) : '')) ?>">Wochen</a>
<a class="archive-switcher__item <?= $archiveView === 'months' ? 'active' : '' ?>" href="<?= e('/archive?view=months' . ($archiveFilterMonth !== '' ? '&filter_month=' . rawurlencode($archiveFilterMonth) : '')) ?>">Monate</a>
</nav>
<form method="post" action="/archive">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="generate_monthly_summary">
<input type="hidden" name="month_key" value="<?= e((string) $month['summary_key']) ?>">
<button class="ghost-button ghost-button--small" type="submit" <?= !$month['can_generate'] || empty($aiAvailable) ? 'disabled' : '' ?>>
<?= !empty($month['has_summary']) ? 'Neu generieren' : 'KI-Monatszusammenfassung erzeugen' ?>
</button>
<form method="get" action="/archive" class="archive-filter">
<input type="hidden" name="view" value="<?= e($archiveView) ?>">
<label>
<span>Zeitraum</span>
<select name="filter_month" onchange="this.form.submit()">
<option value="">Alle Monate</option>
<?php foreach ($archiveMonthOptions as $monthOption): ?>
<option value="<?= e($monthOption) ?>" <?= $archiveFilterMonth === $monthOption ? 'selected' : '' ?>><?= e(month_label($monthOption)) ?></option>
<?php endforeach; ?>
</select>
</label>
</form>
</div>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<section class="archive-summary-section">
<div class="section-head section-head--compact">
<div>
<p class="eyebrow">KI</p>
<h4>Wochenzusammenfassungen</h4>
</div>
</div>
<?php if ($weeklyArchive === []): ?>
<p class="empty-state">Noch keine Wochen verfügbar. Sobald Einträge vorliegen, kannst du hier Wochenrückblicke erzeugen.</p>
<?php else: ?>
<div class="archive-summary-grid">
<?php foreach ($weeklyArchive as $week): ?>
<article class="archive-summary-card">
<div class="archive-summary-card__head">
<div>
<span class="summary-badge">KI</span>
<strong><?= e($week['label']) ?></strong>
</div>
<?php if (!empty($week['has_summary'])): ?>
<span class="chart-chip">vorhanden</span>
<?php else: ?>
<span class="chart-chip chart-chip--muted">offen</span>
<?php endif; ?>
</div>
<p class="helper-text"><?= e($week['date_from']) ?> bis <?= e($week['date_to']) ?></p>
<p class="helper-text"><?= e((string) $week['note_entries_count']) ?> Texteinträge · <?= e((string) $week['tracked_days']) ?> getrackte Tage</p>
<?php if (!empty($week['summary'])): ?>
<p class="helper-text">Erstellt am <?= e(format_display_datetime((string) $week['summary']['created_at'])) ?></p>
<?php else: ?>
<p class="helper-text">Mindestens 3 Texteinträge nötig.</p>
<?php endif; ?>
<div class="archive-item__actions archive-item__actions--stack">
<?php if (!empty($week['summary'])): ?>
<a class="ghost-link archive-action" href="/archive?summary_kind=weekly&amp;summary_key=<?= e(rawurlencode((string) $week['summary_key'])) ?>">Öffnen</a>
<?php endif; ?>
<form method="post" action="/archive">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="generate_weekly_summary">
<input type="hidden" name="week_key" value="<?= e((string) $week['summary_key']) ?>">
<button class="ghost-button ghost-button--small" type="submit" <?= !$week['can_generate'] || empty($aiAvailable) ? 'disabled' : '' ?>>
<?= !empty($week['has_summary']) ? 'Neu generieren' : 'KI-Wochenzusammenfassung erzeugen' ?>
</button>
</form>
</div>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<section class="archive-summary-section">
<div class="section-head section-head--compact">
<div class="archive-workspace">
<section class="archive-main">
<?php if ($archiveView === 'days'): ?>
<div class="archive-list-header">
<div>
<p class="eyebrow">Tage</p>
<h4>Alle gespeicherten Tage</h4>
<h4>Gespeicherte Tage</h4>
</div>
<span class="chart-chip"><?= e((string) count($entries)) ?> Tage</span>
</div>
<?php if ($entries === []): ?>
<p class="empty-state">Noch keine Einträge vorhanden. Auf der Tracking-Seite kannst du den ersten Tag anlegen.</p>
<p class="empty-state">Für diesen Zeitraum gibt es noch keine getrackten Tage.</p>
<?php else: ?>
<div class="archive-items">
<div class="archive-rows">
<?php foreach ($entries as $entry): ?>
<article class="archive-item <?= $selectedEntry !== null && $selectedEntry['date'] === $entry['date'] ? 'active' : '' ?>">
<div>
<strong><?= e(format_display_date($entry['date'], false)) ?></strong>
<a class="archive-row archive-row--day <?= $selectedEntry !== null && $selectedEntry['date'] === $entry['date'] ? 'active' : '' ?>" href="<?= e($archiveUrl(['date' => $entry['date'], 'week' => null, 'month_key' => null])) ?>">
<div class="archive-row__main">
<strong><?= e(format_compact_date($entry['date'])) ?></strong>
<span><?= e($entry['evaluation']['label']) ?></span>
<?php if ((int) $entry['sport_minutes'] > 0 && !empty($entry['sport_type_meta'])): ?>
<span class="sport-pill-group">
<?php foreach ($entry['sport_type_meta'] as $sportType): ?>
<span class="sport-pill">
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
<span><?= e($sportType['label']) ?><?= !empty($sportType['location']) ? ' · ' . e(sport_location_label((string) $sportType['location'])) : '' ?></span>
</span>
<?php endforeach; ?>
</span>
<?php endif; ?>
</div>
<div class="archive-item__meta">
<span><?= e(format_points((float) $entry['evaluation']['total'])) ?></span>
<div class="archive-row__meta">
<span><?= e(format_points((float) $entry['evaluation']['total'])) ?> Punkte</span>
<span>Stimmung <?= e((string) $entry['mood']) ?>/10</span>
</div>
<div class="archive-item__actions">
<a class="ghost-link archive-action" href="/archive?date=<?= e(rawurlencode($entry['date'])) ?>">Ansehen</a>
<a class="ghost-link archive-action" href="/track?date=<?= e(rawurlencode($entry['date'])) ?>">Bearbeiten</a>
</div>
</article>
<span class="archive-row__hint">Ansehen</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
</article>
<aside class="stack-column">
<?php if ($selectedSummary !== null): ?>
<article class="glass-panel detail-card">
<p class="eyebrow">KI-Zusammenfassung</p>
<h3><?= e($selectedSummary['title']) ?></h3>
<p class="hero-label"><?= e($selectedSummary['date_from']) ?> bis <?= e($selectedSummary['date_to']) ?></p>
<p class="helper-text">Erstellt am <?= e(format_display_datetime((string) $selectedSummary['created_at'])) ?></p>
<div class="note-box note-box--summary">
<h4>Text</h4>
<p><?= e($selectedSummary['text']) ?></p>
<?php elseif ($archiveView === 'weeks'): ?>
<div class="archive-list-header">
<div>
<p class="eyebrow">Wochen</p>
<h4>Wöchentliche KI-Rückblicke</h4>
</div>
</article>
<?php elseif ($selectedEntry !== null): ?>
<article class="glass-panel detail-card">
<p class="eyebrow">Ausgewählt</p>
<h3><?= e(format_display_date($selectedEntry['date'])) ?></h3>
<span class="chart-chip"><?= e((string) count($weeklyArchive)) ?> Wochen</span>
</div>
<?php if ($weeklyArchive === []): ?>
<p class="empty-state">Für diesen Zeitraum sind noch keine Wochen im Archiv vorhanden.</p>
<?php else: ?>
<div class="archive-rows archive-rows--summary">
<?php foreach ($weeklyArchive as $week): ?>
<a class="archive-row archive-row--summary archive-row--week <?= $selectedWeek !== null && $selectedWeek['summary_key'] === $week['summary_key'] ? 'active' : '' ?>" href="<?= e($archiveUrl(['week' => $week['summary_key'], 'date' => null, 'month_key' => null])) ?>">
<div class="archive-row__main archive-row__main--week">
<div class="archive-row__title-group">
<strong><?= e($week['label']) ?></strong>
<span><?= e(format_compact_date((string) $week['date_from'])) ?> bis <?= e(format_compact_date((string) $week['date_to'])) ?></span>
</div>
<span class="status-badge status-badge--<?= e($week['status_tone']) ?>"><?= e($week['status_label']) ?></span>
</div>
<div class="archive-row__meta archive-row__meta--stack">
<span><?= e((string) $week['note_entries_count']) ?> Texteinträge</span>
<span><?= e((string) $week['tracked_days']) ?> getrackte Tage</span>
<span><?= e($week['trend_label']) ?></span>
</div>
<span class="archive-row__hint"><?= !empty($week['has_summary']) ? 'Öffnen' : 'Details' ?></span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="archive-list-header">
<div>
<p class="eyebrow">Monate</p>
<h4>Monatliche KI-Rückblicke</h4>
</div>
<span class="chart-chip"><?= e((string) count($monthlyArchive)) ?> Monate</span>
</div>
<?php if ($monthlyArchive === []): ?>
<p class="empty-state">Für diesen Zeitraum sind noch keine Monatsobjekte im Archiv vorhanden.</p>
<?php else: ?>
<div class="archive-rows archive-rows--summary">
<?php foreach ($monthlyArchive as $month): ?>
<a class="archive-row archive-row--summary archive-row--month <?= $selectedMonth !== null && $selectedMonth['summary_key'] === $month['summary_key'] ? 'active' : '' ?>" href="<?= e($archiveUrl(['month_key' => $month['summary_key'], 'date' => null, 'week' => null])) ?>">
<div class="archive-row__main archive-row__main--month">
<div class="archive-row__title-group">
<strong><?= e($month['label']) ?></strong>
<span><?= e(format_compact_date((string) $month['date_from'])) ?> bis <?= e(format_compact_date((string) $month['date_to'])) ?></span>
</div>
<span class="status-badge status-badge--<?= e($month['status_tone']) ?>"><?= e($month['status_label']) ?></span>
</div>
<div class="archive-row__meta archive-row__meta--stack">
<span><?= e($month['weekly_progress_label']) ?></span>
<span><?= e((string) $month['tracked_days']) ?> getrackte Tage</span>
</div>
<span class="archive-row__hint"><?= !empty($month['has_summary']) ? 'Öffnen' : 'Details' ?></span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
<aside class="archive-detail <?= $detailOpen ? 'is-open' : '' ?>" id="archive-detail-panel" data-detail-open="<?= $detailOpen ? '1' : '0' ?>">
<div class="glass-panel archive-detail__panel">
<div class="archive-detail__top">
<div>
<p class="eyebrow">Details</p>
<?php if ($detailType === 'day'): ?>
<h3><?= e(format_compact_date($selectedEntry['date'])) ?></h3>
<?php elseif ($detailType === 'week'): ?>
<h3><?= e($selectedWeek['label']) ?></h3>
<?php elseif ($detailType === 'month'): ?>
<h3><?= e($selectedMonth['label']) ?></h3>
<?php else: ?>
<h3>Archivansicht</h3>
<?php endif; ?>
</div>
<?php if ($detailOpen): ?>
<a class="ghost-link archive-detail__close" href="<?= e($archiveUrl(['date' => null, 'week' => null, 'month_key' => null])) ?>">Schließen</a>
<?php endif; ?>
</div>
<?php if ($detailType === 'day'): ?>
<p class="hero-label"><?= e($selectedEntry['evaluation']['label']) ?> · <?= e(format_points((float) $selectedEntry['evaluation']['total'])) ?> Punkte</p>
<a class="primary-button button-link" href="/track?date=<?= e(rawurlencode($selectedEntry['date'])) ?>">Diesen Tag bearbeiten</a>
<dl class="detail-grid">
<dl class="detail-grid detail-grid--archive-day">
<div><dt>Stimmung</dt><dd><?= e((string) $selectedEntry['mood']) ?>/10</dd></div>
<div><dt>Energie</dt><dd><?= e((string) $selectedEntry['energy']) ?>/10</dd></div>
<div><dt>Stress</dt><dd><?= e((string) $selectedEntry['stress']) ?>/10</dd></div>
@@ -194,24 +173,6 @@
<div><dt>Schlaf</dt><dd><?= e((string) $selectedEntry['sleep_hours']) ?> h</dd></div>
<div><dt>Schlafgefühl</dt><dd><?= e((string) $selectedEntry['sleep_feeling']) ?>/5</dd></div>
<div><dt>Sport</dt><dd><?= e((string) $selectedEntry['sport_minutes']) ?> min</dd></div>
<div>
<dt>Sportarten</dt>
<dd>
<?php if ((int) $selectedEntry['sport_minutes'] > 0 && !empty($selectedEntry['sport_type_meta'])): ?>
<span class="sport-pill-group sport-pill-group--inline">
<?php foreach ($selectedEntry['sport_type_meta'] as $sportType): ?>
<span class="sport-pill sport-pill--inline">
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
<span><?= e($sportType['label']) ?><?= !empty($sportType['location']) ? ' · ' . e(sport_location_label((string) $sportType['location'])) : '' ?></span>
</span>
<?php endforeach; ?>
</span>
<?php else: ?>
keine
<?php endif; ?>
</dd>
</div>
<div><dt>Sportbonus</dt><dd><?= e(format_points((float) ($selectedEntry['evaluation']['components']['sport_bonus'] ?? 0))) ?></dd></div>
<div><dt>Spaziergang</dt><dd><?= e(format_walk_value($selectedEntry)) ?></dd></div>
<div><dt>Alkohol</dt><dd><?= !empty($selectedEntry['alcohol']) ? 'ja' : 'nein' ?></dd></div>
</dl>
@@ -220,13 +181,118 @@
<h4>Notiz</h4>
<p><?= nl2br(e($selectedEntry['note'] !== '' ? $selectedEntry['note'] : 'Keine Notiz hinterlegt.')) ?></p>
</div>
</article>
<?php else: ?>
<article class="glass-panel detail-card">
<p class="eyebrow">Details</p>
<h3>Archivansicht</h3>
<p>Wähle links einen Tag oder eine KI-Zusammenfassung aus. Wochenrückblicke benötigen mindestens 3 Texteinträge, Monatsrückblicke mindestens 2 vorhandene KI-Wochenzusammenfassungen.</p>
</article>
<?php elseif ($detailType === 'week'): ?>
<p class="hero-label"><?= e(format_compact_date((string) $selectedWeek['date_from'])) ?> bis <?= e(format_compact_date((string) $selectedWeek['date_to'])) ?></p>
<div class="archive-detail__status-row">
<span class="status-badge status-badge--<?= e($selectedWeek['status_tone']) ?>"><?= e($selectedWeek['status_label']) ?></span>
<span class="chart-chip"><?= e($selectedWeek['trend_label']) ?></span>
</div>
<dl class="detail-grid detail-grid--archive">
<div><dt>Texteinträge</dt><dd><?= e((string) $selectedWeek['note_entries_count']) ?></dd></div>
<div><dt>Getrackte Tage</dt><dd><?= e((string) $selectedWeek['tracked_days']) ?></dd></div>
<?php if (!empty($selectedWeek['summary'])): ?>
<div><dt>Erstellt am</dt><dd><?= e(format_compact_datetime((string) $selectedWeek['summary']['created_at'])) ?></dd></div>
<?php endif; ?>
</dl>
<div class="note-box archive-detail__status-note">
<h4>KI-Status</h4>
<p><?= e($selectedWeek['status_hint']) ?></p>
</div>
<?php if (!empty($selectedWeek['summary'])): ?>
<div class="archive-detail__actions">
<a class="ghost-link archive-action" href="<?= e($archiveUrl(['week' => $selectedWeek['summary_key'], 'date' => null, 'month_key' => null])) ?>">Öffnen</a>
<form method="post" action="/archive">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="generate_weekly_summary">
<input type="hidden" name="view" value="weeks">
<input type="hidden" name="filter_month" value="<?= e($archiveFilterMonth) ?>">
<input type="hidden" name="week_key" value="<?= e((string) $selectedWeek['summary_key']) ?>">
<button class="ghost-button" type="submit" <?= empty($aiAvailable) ? 'disabled' : '' ?>>Neu generieren</button>
</form>
</div>
<div class="note-box note-box--summary">
<h4>KI-Wochenzusammenfassung</h4>
<p><?= e((string) ($selectedWeek['summary']['text'] ?? '')) ?></p>
</div>
<?php else: ?>
<form method="post" action="/archive" class="archive-detail__single-action">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="generate_weekly_summary">
<input type="hidden" name="view" value="weeks">
<input type="hidden" name="filter_month" value="<?= e($archiveFilterMonth) ?>">
<input type="hidden" name="week_key" value="<?= e((string) $selectedWeek['summary_key']) ?>">
<button class="primary-button" type="submit" <?= !$selectedWeek['can_generate'] || empty($aiAvailable) ? 'disabled' : '' ?>>KI-Wochenzusammenfassung erzeugen</button>
</form>
<?php endif; ?>
<?php elseif ($detailType === 'month'): ?>
<p class="hero-label"><?= e(format_compact_date((string) $selectedMonth['date_from'])) ?> bis <?= e(format_compact_date((string) $selectedMonth['date_to'])) ?></p>
<div class="archive-detail__status-row">
<span class="status-badge status-badge--<?= e($selectedMonth['status_tone']) ?>"><?= e($selectedMonth['status_label']) ?></span>
<span class="chart-chip"><?= e($selectedMonth['weekly_progress_label']) ?></span>
</div>
<dl class="detail-grid detail-grid--archive">
<div><dt>KI-Wochen vorhanden</dt><dd><?= e((string) $selectedMonth['weekly_summary_count']) ?> / <?= e((string) ((int) $selectedMonth['weekly_total_count'])) ?></dd></div>
<?php if (!empty($selectedMonth['summary'])): ?>
<div><dt>Erstellt am</dt><dd><?= e(format_compact_datetime((string) $selectedMonth['summary']['created_at'])) ?></dd></div>
<?php endif; ?>
</dl>
<div class="note-box archive-detail__status-note">
<h4>Monatsstatus</h4>
<p><?= e($selectedMonth['status_hint']) ?></p>
</div>
<div class="note-box archive-detail__week-status">
<h4>Wochen in diesem Monat</h4>
<div class="archive-mini-list">
<?php foreach ($selectedMonth['weeks'] as $week): ?>
<div class="archive-mini-list__row">
<span><?= e($week['label']) ?></span>
<span class="status-badge status-badge--<?= !empty($week['has_summary']) ? 'ready' : 'blocked' ?>"><?= !empty($week['has_summary']) ? 'vorhanden' : 'fehlt' ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php if (!empty($selectedMonth['summary'])): ?>
<div class="archive-detail__actions">
<a class="ghost-link archive-action" href="<?= e($archiveUrl(['month_key' => $selectedMonth['summary_key'], 'date' => null, 'week' => null])) ?>">Öffnen</a>
<form method="post" action="/archive">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="generate_monthly_summary">
<input type="hidden" name="view" value="months">
<input type="hidden" name="filter_month" value="<?= e($archiveFilterMonth) ?>">
<input type="hidden" name="month_key" value="<?= e((string) $selectedMonth['summary_key']) ?>">
<button class="ghost-button" type="submit" <?= empty($aiAvailable) ? 'disabled' : '' ?>>Neu generieren</button>
</form>
</div>
<div class="note-box note-box--summary">
<h4>KI-Monatszusammenfassung</h4>
<p><?= e((string) ($selectedMonth['summary']['text'] ?? '')) ?></p>
</div>
<?php else: ?>
<form method="post" action="/archive" class="archive-detail__single-action">
<?= csrf_field() ?>
<input type="hidden" name="form_name" value="generate_monthly_summary">
<input type="hidden" name="view" value="months">
<input type="hidden" name="filter_month" value="<?= e($archiveFilterMonth) ?>">
<input type="hidden" name="month_key" value="<?= e((string) $selectedMonth['summary_key']) ?>">
<button class="primary-button" type="submit" <?= !$selectedMonth['can_generate'] || empty($aiAvailable) ? 'disabled' : '' ?>>KI-Monatszusammenfassung erzeugen</button>
</form>
<?php endif; ?>
<?php else: ?>
<p class="helper-text">Wähle links einen Tag, eine Woche oder einen Monat aus.</p>
<?php endif; ?>
</div>
</aside>
</div>
</article>
</section>