feat(reports): add created_on timestamps and detail links to file downloads report

Show upload timestamp for every file; bold the newest upload per session/presenter
group so staff can quickly identify the most recent version. Add Session/Presenter
navigation links in each card header for direct access without searching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-11 04:23:07 -04:00
parent 246d4f8ef3
commit a5beff4aa8

View File

@@ -27,6 +27,7 @@ import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
import {
ArrowUpDown,
Download,
ExternalLink,
FileText,
LoaderCircle,
RefreshCw,
@@ -148,6 +149,11 @@ function build_filename(file: any, fmt: FormatKey): string {
return `${pre}${stem}${suf}.${ext}`;
}
// Returns the largest created_on string in a file list — identifies the newest upload per group.
function newest_in_group(files: { created_on?: string | null }[]): string {
return files.reduce((max, f) => ((f.created_on ?? '') > max ? (f.created_on ?? '') : max), '');
}
// Strip :443 from https URLs — redundant and clutters shareable links.
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
@@ -416,6 +422,7 @@ async function handle_qry() {
{:else}
{#each session_groups as sg (sg.session_id)}
{@const has_any = sg.session_files.length > 0 || sg.presenter_groups.length > 0}
{@const newest_session_ts = newest_in_group(sg.session_files)}
{#if has_any}
<!-- Session card -->
<section class="border-surface-300-700 bg-surface-50-900 my-2 rounded-lg border">
@@ -435,6 +442,15 @@ async function handle_qry() {
{sg.session_files.length} session file{sg.session_files.length !== 1 ? 's' : ''},
&nbsp;{sg.presenter_groups.reduce((n, pg) => n + pg.files.length, 0)} presenter file{sg.presenter_groups.reduce((n, pg) => n + pg.files.length, 0) !== 1 ? 's' : ''}
</span>
{#if sg.session_id !== '__no_session__'}
<a
href="/events/{$events_slct.event_id}/session/{sg.session_id}"
class="btn btn-xs preset-tonal-surface shrink-0 opacity-50 hover:opacity-100 transition-opacity"
title="View session: {sg.session_name}">
<ExternalLink size="0.8em" class="mr-0.5" />
Session
</a>
{/if}
</div>
<div class="divide-surface-200-800 divide-y px-2 py-1">
@@ -452,6 +468,7 @@ async function handle_qry() {
{@const computed_name = build_filename(file, selected_format)}
{@const dl_url = build_download_url(file, selected_format)}
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
{@const is_newest = !!file.created_on && file.created_on === newest_session_ts}
<div class="border-surface-100-900 hover:bg-surface-100-900 flex flex-row items-center gap-2 border-t py-1.5 transition-colors">
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
<div class="flex min-w-0 grow flex-col gap-0.5">
@@ -466,6 +483,16 @@ async function handle_qry() {
title="{computed_name}{is_long ? ` (${computed_name.length} chars may be long for some systems)` : ''}">
{computed_name}
</span>
{#if file.created_on}
<span
class="tabular-nums text-xs"
class:opacity-40={!is_newest}
class:font-bold={is_newest}
title="Uploaded: {file.created_on}">
{ae_util.iso_datetime_formatter(file.created_on, 'date_iso')}
&nbsp;{ae_util.iso_datetime_formatter(file.created_on, 'time_12_short_no_leading')}
</span>
{/if}
</div>
<!-- File size -->
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
@@ -496,21 +523,35 @@ async function handle_qry() {
<!-- ---- Presenter-level files ---- -->
{#each sg.presenter_groups as pg (pg.presenter_id)}
{@const newest_presenter_ts = newest_in_group(pg.files)}
<div class="py-2">
<h4 class="mb-1 flex items-center gap-1 text-xs font-bold tracking-wider opacity-60">
<User size="0.9em" />
{pg.presenter_full_name || '— presenter name not set —'}
{#if pg.presentation_name}
<span class="font-normal normal-case opacity-80">{pg.presentation_name}</span>
<!-- Wrap h4 + link so the link sits outside the opacity-60 container -->
<div class="mb-1 flex items-center gap-2">
<h4 class="flex grow items-center gap-1 text-xs font-bold tracking-wider opacity-60">
<User size="0.9em" />
{pg.presenter_full_name || '— presenter name not set —'}
{#if pg.presentation_name}
<span class="font-normal normal-case opacity-80">{pg.presentation_name}</span>
{/if}
<span class="font-normal normal-case">({pg.files.length} file{pg.files.length !== 1 ? 's' : ''})</span>
</h4>
{#if pg.presenter_id !== '__unknown__'}
<a
href="/events/{$events_slct.event_id}/presenter/{pg.presenter_id}"
class="btn btn-xs preset-tonal-surface shrink-0 opacity-50 hover:opacity-100 transition-opacity"
title="View presenter: {pg.presenter_full_name}">
<ExternalLink size="0.75em" class="mr-0.5" />
Presenter
</a>
{/if}
<span class="font-normal normal-case">({pg.files.length} file{pg.files.length !== 1 ? 's' : ''})</span>
</h4>
</div>
<div class="flex flex-col">
{#each pg.files as file (file.event_file_id)}
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
{@const computed_name = build_filename(file, selected_format)}
{@const dl_url = build_download_url(file, selected_format)}
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
{@const is_newest = !!file.created_on && file.created_on === newest_presenter_ts}
<div class="border-surface-100-900 hover:bg-surface-100-900 flex flex-row items-center gap-2 border-t py-1.5 transition-colors">
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
<div class="flex min-w-0 grow flex-col gap-0.5">
@@ -525,6 +566,16 @@ async function handle_qry() {
title="{computed_name}{is_long ? ` (${computed_name.length} chars may be long for some systems)` : ''}">
{computed_name}
</span>
{#if file.created_on}
<span
class="tabular-nums text-xs"
class:opacity-40={!is_newest}
class:font-bold={is_newest}
title="Uploaded: {file.created_on}">
{ae_util.iso_datetime_formatter(file.created_on, 'date_iso')}
&nbsp;{ae_util.iso_datetime_formatter(file.created_on, 'time_12_short_no_leading')}
</span>
{/if}
</div>
<!-- File size -->
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">