refactor(idaa): filter bar polish — cycling sort, centered layout, move into form

- Replace three sort chips with single cycling button (Last Updated → Name A→Z → Name Z→A → repeat)
- Add min-w-36 to sort button to prevent layout bounce between labels
- Move My Meetings chip to filter row (first position) alongside Virtual/In-Person
- Widen filter chips row from max-w-xl to max-w-2xl to match search bar
- Move sort/max/actions section inside <form> so all rows share the same width constraint
- Flatten span wrappers in bottom row — all buttons are direct flex children of justify-center container, fixing left-drift when conditional buttons are hidden
- Add keyed {#each (val)} to Type chip loop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-18 17:50:43 -04:00
parent 6857f1226c
commit 429f38996a

View File

@@ -65,21 +65,22 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
}; };
} }
function set_sort_mode(mode: string) { const sort_modes = [
$idaa_loc.recovery_meetings.qry__order_by = mode; { mode: 'updated_on', label: 'Last Updated', icon: 'fa-clock', order_by_li: { priority: 'DESC', sort: 'DESC', updated_on: 'DESC', created_on: 'DESC', name: 'ASC' } },
if (mode === 'updated_on') { { mode: 'name_asc', label: 'Name A→Z', icon: 'fa-sort-alpha-down', order_by_li: { priority: 'DESC', sort: 'DESC', name: 'ASC', updated_on: 'DESC', created_on: 'DESC' } },
$idaa_loc.recovery_meetings.qry__order_by_li = { { mode: 'name_desc', label: 'Name Z→A', icon: 'fa-sort-alpha-up-alt', order_by_li: { priority: 'DESC', sort: 'DESC', name: 'DESC', updated_on: 'DESC', created_on: 'DESC' } }
priority: 'DESC', sort: 'DESC', updated_on: 'DESC', created_on: 'DESC', name: 'ASC' ];
};
} else if (mode === 'name_asc') { let sort_cycle_idx = $derived.by(() => {
$idaa_loc.recovery_meetings.qry__order_by_li = { const idx = sort_modes.findIndex(m => m.mode === $idaa_loc.recovery_meetings.qry__order_by);
priority: 'DESC', sort: 'DESC', name: 'ASC', updated_on: 'DESC', created_on: 'DESC' return idx >= 0 ? idx : 0;
}; });
} else if (mode === 'name_desc') { let current_sort = $derived(sort_modes[sort_cycle_idx]);
$idaa_loc.recovery_meetings.qry__order_by_li = {
priority: 'DESC', sort: 'DESC', name: 'DESC', updated_on: 'DESC', created_on: 'DESC' function cycle_sort() {
}; const next = sort_modes[(sort_cycle_idx + 1) % sort_modes.length];
} $idaa_loc.recovery_meetings.qry__order_by = next.mode;
$idaa_loc.recovery_meetings.qry__order_by_li = next.order_by_li;
handle_search_trigger(); handle_search_trigger();
} }
</script> </script>
@@ -186,9 +187,29 @@ function set_sort_mode(mode: string) {
</button> </button>
</div> </div>
<!-- Filter chips: Location + Type --> <!-- Filter chips: My Meetings + Location + Type -->
<div <div
class="border-surface-300-700 flex w-full max-w-xl flex-row flex-wrap items-center justify-start gap-x-4 gap-y-1 border-b p-1"> class="border-surface-300-700 flex w-full max-w-2xl flex-row flex-wrap items-center justify-center gap-x-4 gap-y-1 border-b p-1">
<!-- My Meetings: favorites filter — shown only to logged-in Novi members -->
{#if $idaa_loc.novi_uuid}
<button
type="button"
onclick={() => {
$idaa_loc.recovery_meetings.qry__favorites_only =
!$idaa_loc.recovery_meetings.qry__favorites_only;
}}
class="novi_btn btn-star btn btn-sm transition-all
{$idaa_loc.recovery_meetings.qry__favorites_only
? 'preset-filled-warning-300-700'
: 'preset-outlined-surface-300-700 opacity-60 hover:opacity-100'}"
title={$idaa_loc.recovery_meetings.qry__favorites_only
? 'Showing My Meetings only — click to show all'
: 'Show only My Meetings (starred)'}>
<span class="fas fa-star m-1 {$idaa_loc.recovery_meetings.qry__favorites_only ? 'text-yellow-600' : ''}"></span>
My Meetings
</button>
{/if}
<!-- Location: independent toggles (a meeting can be both virtual and in-person) --> <!-- Location: independent toggles (a meeting can be both virtual and in-person) -->
<div class="flex flex-row flex-wrap items-center gap-1"> <div class="flex flex-row flex-wrap items-center gap-1">
@@ -225,7 +246,7 @@ function set_sort_mode(mode: string) {
['IDAA', 'IDAA', 'Open to IDAA members only'], ['IDAA', 'IDAA', 'Open to IDAA members only'],
['Caduceus', 'Caduceus', 'Open to all healthcare workers including those who do not qualify for IDAA'], ['Caduceus', 'Caduceus', 'Open to all healthcare workers including those who do not qualify for IDAA'],
['Family Recovery', 'Family Recovery', 'Open to spouses, parents, and children of medical professionals in recovery'] ['Family Recovery', 'Family Recovery', 'Open to spouses, parents, and children of medical professionals in recovery']
] as [val, label, tip]} ] as [val, label, tip] (val)}
{@const active = val === '' ? !$idaa_loc.recovery_meetings.qry__type : $idaa_loc.recovery_meetings.qry__type === val} {@const active = val === '' ? !$idaa_loc.recovery_meetings.qry__type : $idaa_loc.recovery_meetings.qry__type === val}
<button <button
type="button" type="button"
@@ -257,268 +278,200 @@ function set_sort_mode(mode: string) {
</label> </label>
{/if} {/if}
</div> </div>
</form>
<div <div
class="ae_group ae_row flex w-full max-w-full flex-row flex-wrap items-center justify-center gap-2 p-1"> class="ae_group ae_row flex w-full flex-row flex-wrap items-center justify-center gap-2 p-1">
<!-- Sort chips + Max results stepper --> <!-- Sort: single cycling button -->
<span class="flex flex-row flex-wrap items-center justify-center gap-x-4 gap-y-1"> <div class="flex flex-row items-center gap-1">
<span class="sr-only">Sort:</span>
<button
type="button"
onclick={cycle_sort}
title="Currently: {current_sort.label} click to change sort order"
class="novi_btn btn btn-sm preset-outlined-surface-300-700 opacity-80 hover:opacity-100 transition-all min-w-36">
<span class="fas {current_sort.icon} mr-1"></span>{current_sort.label}
<span class="fas fa-redo fa-xs ml-1 opacity-40"></span>
</button>
</div>
<!-- Sort: mutually exclusive chips --> <!-- Max results: +/- stepper through predefined steps -->
<div class="flex flex-row flex-wrap items-center gap-1"> <div class="flex flex-row items-center gap-1">
<span class="sr-only">Sort:</span> <span class="sr-only">Max results:</span>
{#each [ <button
['updated_on', 'Last Updated', 'fa-clock'], type="button"
['name_asc', 'Name AZ', 'fa-sort-alpha-down'], onclick={() => { if (limit_idx > 0) $idaa_loc.recovery_meetings.qry__limit = limit_steps[limit_idx - 1]; }}
['name_desc', 'Name ZA', 'fa-sort-alpha-up-alt'] disabled={limit_idx <= 0}
] as [mode, label, icon]} title="Show fewer results"
{@const active = $idaa_loc.recovery_meetings.qry__order_by === mode} class="novi_btn btn btn-sm preset-outlined-surface-300-700 transition-all
<button {limit_idx <= 0 ? 'opacity-30 cursor-not-allowed' : 'opacity-60 hover:opacity-100'}">
type="button" <span class="fas fa-minus"></span>
onclick={() => set_sort_mode(mode)} </button>
aria-pressed={active} <span
title="Sort by {label}" class="min-w-10 text-center text-sm font-semibold tabular-nums"
class="novi_btn btn btn-sm transition-all title="Max results shown">
{active {$idaa_loc.recovery_meetings.qry__limit}
? 'preset-filled-tertiary-400-600' </span>
: 'preset-outlined-surface-300-700 opacity-60 hover:opacity-100'}"> <button
<span class="fas {icon} mr-1"></span>{label} type="button"
</button> onclick={() => { if (limit_idx < limit_steps.length - 1) $idaa_loc.recovery_meetings.qry__limit = limit_steps[limit_idx + 1]; }}
{/each} disabled={limit_idx >= limit_steps.length - 1}
</div> title="Show more results"
class="novi_btn btn btn-sm preset-outlined-surface-300-700 transition-all
{limit_idx >= limit_steps.length - 1 ? 'opacity-30 cursor-not-allowed' : 'opacity-60 hover:opacity-100'}">
<span class="fas fa-plus"></span>
</button>
</div>
<!-- Max results: +/- stepper through predefined steps --> <!-- Staff: toggle hidden events visibility (trusted + edit mode) -->
<div class="flex flex-row items-center gap-1"> {#if $ae_loc.edit_mode && $ae_loc.trusted_access && (!$idaa_loc.recovery_meetings.qry__hidden || $idaa_loc.recovery_meetings.qry__hidden == 'not_hidden')}
<span class="sr-only">Max results:</span> <button
<button type="button"
type="button" onclick={() => {
onclick={() => { if (limit_idx > 0) $idaa_loc.recovery_meetings.qry__limit = limit_steps[limit_idx - 1]; }} $idaa_loc.recovery_meetings.qry__hidden = 'all';
disabled={limit_idx <= 0} $idaa_loc.recovery_meetings.qry__limit = 200;
title="Show fewer results" }}
class="novi_btn btn btn-sm preset-outlined-surface-300-700 transition-all class="novi_btn btn_show_recovery_mtg_event ae_btn btn-info btn btn-sm
{limit_idx <= 0 ? 'opacity-30 cursor-not-allowed' : 'opacity-60 hover:opacity-100'}"> preset-tonal-secondary preset-outlined-secondary-200-800 hover:preset-filled-secondary-200-800 transition">
<span class="fas fa-minus"></span> <span class="fas fa-eye m-1"></span> Show Hidden Events
</button> </button>
<span {:else if $ae_loc.trusted_access && $idaa_loc.recovery_meetings.qry__hidden != 'not_hidden'}
class="min-w-10 text-center text-sm font-semibold tabular-nums" <button
title="Max results shown"> type="button"
{$idaa_loc.recovery_meetings.qry__limit} onclick={() => { $idaa_loc.recovery_meetings.qry__hidden = 'not_hidden'; }}
</span> class="novi_btn btn_hide_recovery_mtg_event ae_btn btn-info btn btn-sm
<button preset-tonal-secondary preset-outlined-secondary-200-800 hover:preset-filled-secondary-200-800 transition">
type="button" <span class="fas fa-eye-slash m-1"></span> Hide Hidden Events
onclick={() => { if (limit_idx < limit_steps.length - 1) $idaa_loc.recovery_meetings.qry__limit = limit_steps[limit_idx + 1]; }} </button>
disabled={limit_idx >= limit_steps.length - 1} {/if}
title="Show more results"
class="novi_btn btn btn-sm preset-outlined-surface-300-700 transition-all
{limit_idx >= limit_steps.length - 1 ? 'opacity-30 cursor-not-allowed' : 'opacity-60 hover:opacity-100'}">
<span class="fas fa-plus"></span>
</button>
</div>
</span> <!-- Staff: toggle disabled events visibility (manager + edit mode) -->
{#if $ae_loc.edit_mode && $ae_loc.manager_access && (!$idaa_loc.recovery_meetings.qry__enabled || $idaa_loc.recovery_meetings.qry__enabled == 'enabled')}
<button
type="button"
onclick={() => {
$idaa_loc.recovery_meetings.qry__hidden = 'all';
$idaa_loc.recovery_meetings.qry__enabled = 'all';
// NOTE: I may re-enable this limit reset later. It is sometimes useful.
// $idaa_loc.recovery_meetings.qry__limit = 500;
}}
class="novi_btn btn_show_recovery_mtg_event ae_btn btn-warning btn btn-sm
preset-tonal-warning preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 transition">
<span class="fas fa-eye m-1"></span> Show Disabled Events
</button>
{:else if $ae_loc.edit_mode && $ae_loc.manager_access && $idaa_loc.recovery_meetings.qry__enabled != 'enabled'}
<button
type="button"
onclick={() => { $idaa_loc.recovery_meetings.qry__enabled = 'enabled'; }}
class="novi_btn btn_hide_recovery_mtg_event ae_btn btn-warning btn btn-sm
preset-tonal-warning preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 transition">
<span class="fas fa-eye-slash m-1"></span> Hide Disabled Events
</button>
{/if}
<span class="flex flex-row items-center justify-around gap-1"> <!-- Create new meeting (Novi members and trusted staff in edit mode) -->
{#if $ae_loc.edit_mode && $ae_loc.trusted_access && (!$idaa_loc.recovery_meetings.qry__hidden || $idaa_loc.recovery_meetings.qry__hidden == 'not_hidden')} {#if ($ae_loc.trusted_access && $ae_loc.edit_mode) || $idaa_loc.novi_uuid}
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
$idaa_loc.recovery_meetings.qry__hidden = 'all'; if (!confirm('Create new meeting?')) {
$idaa_loc.recovery_meetings.qry__limit = 200; return false;
}} }
class=" $idaa_slct.event_id = null;
novi_btn btn_show_recovery_mtg_event ae_btn btn-info $idaa_slct.event_obj = {};
btn btn-sm
preset-tonal-secondary preset-outlined-secondary-200-800 hover:preset-filled-secondary-200-800
transition
">
<span class="fas fa-eye m-1"></span> Show Hidden Events
</button>
{:else if $ae_loc.trusted_access && $idaa_loc.recovery_meetings.qry__hidden != 'not_hidden'}
<button
type="button"
onclick={() => {
$idaa_loc.recovery_meetings.qry__hidden = 'not_hidden';
}}
class="
novi_btn btn_hide_recovery_mtg_event ae_btn btn-info
btn btn-sm
preset-tonal-secondary preset-outlined-secondary-200-800 hover:preset-filled-secondary-200-800
transition
">
<span class="fas fa-eye-slash m-1"></span> Hide Hidden Events
</button>
{/if}
{#if $ae_loc.edit_mode && $ae_loc.manager_access && (!$idaa_loc.recovery_meetings.qry__enabled || $idaa_loc.recovery_meetings.qry__enabled == 'enabled')} let novi_uuid = $idaa_loc.novi_uuid;
<button if (!novi_uuid && typeof localStorage !== 'undefined') {
type="button" try {
onclick={() => { const ls_val = localStorage.getItem('ae_idaa_loc');
$idaa_loc.recovery_meetings.qry__hidden = 'all'; if (ls_val) {
$idaa_loc.recovery_meetings.qry__enabled = 'all'; const ls_json = JSON.parse(ls_val);
// NOTE: I may re-enable this limit reset later. It is sometimes useful. novi_uuid = ls_json.novi_uuid;
// $idaa_loc.recovery_meetings.qry__limit = 500;
}}
class="
novi_btn btn_show_recovery_mtg_event ae_btn btn-warning
btn btn-sm
preset-tonal-warning preset-outlined-warning-200-800 hover:preset-filled-warning-200-800
transition
">
<span class="fas fa-eye m-1"></span> Show Disabled Events
</button>
{:else if $ae_loc.edit_mode && $ae_loc.manager_access && $idaa_loc.recovery_meetings.qry__enabled != 'enabled'}
<button
type="button"
onclick={() => {
$idaa_loc.recovery_meetings.qry__enabled = 'enabled';
}}
class="
novi_btn btn_hide_recovery_mtg_event ae_btn btn-warning
btn btn-sm
preset-tonal-warning preset-outlined-warning-200-800 hover:preset-filled-warning-200-800
transition
">
<span class="fas fa-eye-slash m-1"></span> Hide Disabled Events
</button>
{/if}
</span>
<span class="flex flex-row items-center justify-around gap-1">
{#if $idaa_loc.novi_uuid}
<button
type="button"
onclick={() => {
$idaa_loc.recovery_meetings.qry__favorites_only =
!$idaa_loc.recovery_meetings.qry__favorites_only;
}}
class="
novi_btn btn-star
btn btn-sm
transition-all
{$idaa_loc.recovery_meetings.qry__favorites_only
? 'preset-filled-warning-300-700'
: 'preset-outlined-surface-300-700 opacity-60 hover:opacity-100'}
"
title={$idaa_loc.recovery_meetings.qry__favorites_only
? 'Showing My Meetings only — click to show all'
: 'Show only My Meetings (starred)'}>
<span class="fas fa-star m-1 {$idaa_loc.recovery_meetings.qry__favorites_only ? 'text-yellow-600' : ''}"></span>
My Meetings
</button>
{/if}
{#if ($ae_loc.trusted_access && $ae_loc.edit_mode) || $idaa_loc.novi_uuid}
<button
type="button"
onclick={() => {
if (!confirm('Create new meeting?')) {
return false;
}
$idaa_slct.event_id = null;
$idaa_slct.event_obj = {};
let novi_uuid = $idaa_loc.novi_uuid;
if (!novi_uuid && typeof localStorage !== 'undefined') {
try {
const ls_val = localStorage.getItem('ae_idaa_loc');
if (ls_val) {
const ls_json = JSON.parse(ls_val);
novi_uuid = ls_json.novi_uuid;
}
} catch (e) {
// Ignore storage errors
} }
} catch {
// Ignore storage errors
} }
}
let data_kv = { let data_kv = {
name: 'Change NEW Recovery Meeting Name', name: 'Change NEW Recovery Meeting Name',
external_person_id: novi_uuid external_person_id: novi_uuid
}; };
if (log_lvl) { if (log_lvl) {
console.log( console.log(
'Creating new event (recovery meeting) with data_kv:', 'Creating new event (recovery meeting) with data_kv:',
data_kv data_kv
); );
} }
events_func events_func
.create_ae_obj__event({ .create_ae_obj__event({
api_cfg: $ae_api, api_cfg: $ae_api,
account_id: $ae_loc.account_id, account_id: $ae_loc.account_id,
data_kv: data_kv, data_kv: data_kv,
log_lvl: log_lvl log_lvl: log_lvl
}) })
.then((results) => { .then((results) => {
if (results) { if (results) {
$idaa_slct.event_id = results.event_id; $idaa_slct.event_id = results.event_id;
$idaa_slct.event_obj = { ...results }; $idaa_slct.event_obj = { ...results };
$idaa_sess.recovery_meetings.edit__event_obj = $idaa_sess.recovery_meetings.edit__event_obj =
results.event_id; results.event_id;
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`New event created with ID: ${results.event_id}` `New event created with ID: ${results.event_id}`
);
}
goto(
`/idaa/recovery_meetings/${results.event_id}`
); );
} }
}) goto(
.catch((error) => { `/idaa/recovery_meetings/${results.event_id}`
console.error('Error creating event:', error); );
alert('Failed to create new event.'); }
}); })
}} .catch((error) => {
class=" console.error('Error creating event:', error);
novi_btn btn-tertiary alert('Failed to create new event.');
btn });
btn-sm preset-tonal-warning }}
preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs class="novi_btn btn-tertiary btn btn-sm preset-tonal-warning
transition preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs transition"
" disabled={!$ae_loc.authenticated_access}>
disabled={!$ae_loc.authenticated_access}> <span class="fas fa-plus m-1"></span> Create New Meeting
<span class="fas fa-plus m-1"></span> Create New Meeting </button>
</button> {/if}
{/if}
{#if $ae_loc.edit_mode && $ae_loc.trusted_access} <!-- Staff: export all meeting data to Excel (trusted + edit mode) -->
<button {#if $ae_loc.edit_mode && $ae_loc.trusted_access}
type="button" <button
onclick={() => { type="button"
if (!confirm('Download exported data Excel file?')) { onclick={() => {
return false; if (!confirm('Download exported data Excel file?')) {
} return false;
ae_promises.download__events_export = }
core_func.download_export__obj_type({ ae_promises.download__events_export =
api_cfg: $ae_api, core_func.download_export__obj_type({
get_obj_type: 'event', api_cfg: $ae_api,
for_obj_type: 'account', get_obj_type: 'event',
for_obj_id: $ae_loc.account_id, for_obj_type: 'account',
exp_alt: 'idaa', for_obj_id: $ae_loc.account_id,
file_type: 'Excel', exp_alt: 'idaa',
return_file: true, file_type: 'Excel',
filename: `${$ae_loc.account_code}_IDAA_Recovery_Meetings_export_${ae_util.iso_datetime_formatter()}.xlsx`, return_file: true,
auto_download: true, filename: `${$ae_loc.account_code}_IDAA_Recovery_Meetings_export_${ae_util.iso_datetime_formatter()}.xlsx`,
log_lvl: 1 auto_download: true,
}); log_lvl: 1
}} });
class=" }}
novi_btn btn-tertiary class="novi_btn btn-tertiary btn btn-sm preset-tonal-warning
btn preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs transition"
btn-sm preset-tonal-warning title={`Download meeting data for ${$ae_loc.account_name}`}>
preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs {#await ae_promises.download__events_export}
transition <span class="fas fa-spinner fa-spin m-1"></span>
" {:then}
title={`Download meeting data for ${$ae_loc.account_name}`}> <span class="fas fa-download m-1"></span>
{#await ae_promises.download__events_export} {/await}
<span class="fas fa-spinner fa-spin m-1"></span> Export All Data
{:then} </button>
<span class="fas fa-download m-1"></span> {/if}
{/await} </div>
Export All Data </form>
</button>
{/if}
</span>
</div>
</section> </section>