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
+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');