refactor(archive): redesign segmented archive experience
This commit is contained in:
+222
-21
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user