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:
@@ -27,6 +27,7 @@ import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
|||||||
import {
|
import {
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
Download,
|
Download,
|
||||||
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -148,6 +149,11 @@ function build_filename(file: any, fmt: FormatKey): string {
|
|||||||
return `${pre}${stem}${suf}.${ext}`;
|
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.
|
// Strip :443 from https URLs — redundant and clutters shareable links.
|
||||||
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
|
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
|
||||||
|
|
||||||
@@ -416,6 +422,7 @@ async function handle_qry() {
|
|||||||
{:else}
|
{:else}
|
||||||
{#each session_groups as sg (sg.session_id)}
|
{#each session_groups as sg (sg.session_id)}
|
||||||
{@const has_any = sg.session_files.length > 0 || sg.presenter_groups.length > 0}
|
{@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}
|
{#if has_any}
|
||||||
<!-- Session card -->
|
<!-- Session card -->
|
||||||
<section class="border-surface-300-700 bg-surface-50-900 my-2 rounded-lg border">
|
<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' : ''},
|
{sg.session_files.length} session file{sg.session_files.length !== 1 ? 's' : ''},
|
||||||
{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' : ''}
|
{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>
|
</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>
|
||||||
|
|
||||||
<div class="divide-surface-200-800 divide-y px-2 py-1">
|
<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 computed_name = build_filename(file, selected_format)}
|
||||||
{@const dl_url = build_download_url(file, selected_format)}
|
{@const dl_url = build_download_url(file, selected_format)}
|
||||||
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
|
{@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">
|
<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) -->
|
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
|
||||||
<div class="flex min-w-0 grow flex-col gap-0.5">
|
<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)` : ''}">
|
title="{computed_name}{is_long ? ` (${computed_name.length} chars — may be long for some systems)` : ''}">
|
||||||
{computed_name}
|
{computed_name}
|
||||||
</span>
|
</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')}
|
||||||
|
{ae_util.iso_datetime_formatter(file.created_on, 'time_12_short_no_leading')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- File size -->
|
<!-- File size -->
|
||||||
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
||||||
@@ -496,21 +523,35 @@ async function handle_qry() {
|
|||||||
|
|
||||||
<!-- ---- Presenter-level files ---- -->
|
<!-- ---- Presenter-level files ---- -->
|
||||||
{#each sg.presenter_groups as pg (pg.presenter_id)}
|
{#each sg.presenter_groups as pg (pg.presenter_id)}
|
||||||
|
{@const newest_presenter_ts = newest_in_group(pg.files)}
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h4 class="mb-1 flex items-center gap-1 text-xs font-bold tracking-wider opacity-60">
|
<!-- Wrap h4 + link so the link sits outside the opacity-60 container -->
|
||||||
<User size="0.9em" />
|
<div class="mb-1 flex items-center gap-2">
|
||||||
{pg.presenter_full_name || '— presenter name not set —'}
|
<h4 class="flex grow items-center gap-1 text-xs font-bold tracking-wider opacity-60">
|
||||||
{#if pg.presentation_name}
|
<User size="0.9em" />
|
||||||
<span class="font-normal normal-case opacity-80">— {pg.presentation_name}</span>
|
{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}
|
{/if}
|
||||||
<span class="font-normal normal-case">({pg.files.length} file{pg.files.length !== 1 ? 's' : ''})</span>
|
</div>
|
||||||
</h4>
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#each pg.files as file (file.event_file_id)}
|
{#each pg.files as file (file.event_file_id)}
|
||||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||||
{@const computed_name = build_filename(file, selected_format)}
|
{@const computed_name = build_filename(file, selected_format)}
|
||||||
{@const dl_url = build_download_url(file, selected_format)}
|
{@const dl_url = build_download_url(file, selected_format)}
|
||||||
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
|
{@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">
|
<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) -->
|
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
|
||||||
<div class="flex min-w-0 grow flex-col gap-0.5">
|
<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)` : ''}">
|
title="{computed_name}{is_long ? ` (${computed_name.length} chars — may be long for some systems)` : ''}">
|
||||||
{computed_name}
|
{computed_name}
|
||||||
</span>
|
</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')}
|
||||||
|
{ae_util.iso_datetime_formatter(file.created_on, 'time_12_short_no_leading')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- File size -->
|
<!-- File size -->
|
||||||
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
||||||
|
|||||||
Reference in New Issue
Block a user