feat(idaa): replace filter checkboxes/radios with toggle pill chips + update docs

- ae_idaa_comp__event_obj_qry.svelte: replace Location checkboxes and Type radio
  inputs with styled pill-chip buttons. Location chips (Virtual / In-Person) are
  independent toggles; Type chips (All / IDAA / Caduceus / Family Recovery) are
  mutually exclusive — clicking the active chip deselects back to All. Chips fire
  the reactive search $effect directly via store updates; no explicit trigger needed.
  Remote First dev toggle preserved in edit mode, now inline with filter chips.
- CLIENT__IDAA_and_customized_mods.md: update Recovery Meetings filter/sort docs,
  add My Meetings / favorites section, correct idaa_loc and idaa_sess store schemas,
  bump Last Verified date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-18 17:08:01 -04:00
parent 3a81887c56
commit 7a1099bbbe
2 changed files with 112 additions and 94 deletions

View File

@@ -376,12 +376,19 @@ Recovery Meetings reuses the Aether Events object to represent AA recovery meeti
### Search Filters
Members can filter meetings by:
- **Fulltext search** — name, location
- **Physical** — in-person meetings
- **Virtual** — online meetings (Zoom, Google Meet, etc.)
- **Meeting type** — specific meeting format categories
- **Fulltext search** — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
- **Virtual** — online meetings (Zoom, Jitsi, other)
- **In-person** — physical location meetings
- **Meeting type** — IDAA / Caduceus / Family Recovery
- **My Meetings** — star toggle; shows only meetings the member has starred (favorites)
Search is debounced (250ms) and uses the standard Aether SWR pattern.
**Sort options:** Last Updated (default), Meeting Name AZ, Meeting Name ZA.
**Empty state behavior:**
- Zero results with active filters → "No meetings found for these filters" + "Clear all filters" button
- Zero results with no filters → bare message shown, then after 8s a "Refresh Meeting Cache" escape hatch appears (clears IDB and re-fetches from API — indicates a stale-cache problem, not a real empty set)
Search uses the standard Aether SWR pattern (IDB cache returned immediately, then API refreshes in background).
### Search Architecture — What Is and Isn't Searched
@@ -398,6 +405,22 @@ has not yet been added to the searchable fields whitelist in the API.
- Frontend: add `contact_li_json_ext` as an OR condition in `search__event()`, and update
the local IDB fast-path filter to parse `contact_li_json` for instant cache results
### My Meetings (Favorites)
Members can star meetings to build a personal "My Meetings" list. The star toggle appears:
- On each card in the meeting list (`ae_idaa_comp__event_obj_li.svelte`)
- On the meeting detail page nav bar (`[event_id]/+page.svelte`)
Favorites are stored in the `data_store` table (code: `idaa_meetings_favorites`, scoped to the
IDAA account). The record's `json` field holds `{ [novi_uuid]: [event_id, ...] }` — one shared
record per account containing all members' favorites. This means:
- Favorites persist across browsers and devices (server-side)
- Does **not** write to `ae_event` rows (avoiding the `ON UPDATE current_timestamp()` side effect)
- Known last-write-wins race condition if two members toggle simultaneously — acceptable for ~1000 members
- Pre-created DB records: ID 150 (`gaTKSVPagFj`, account_id=1, dev/demo), ID 151 (`knJh8zhyKT0`, account_id=13, live IDAA)
The star button uses inline styles (not `.btn`) to avoid Bootstrap v3 box-model overrides in the iframe.
### Edit Form — Sections and Key Fields
The edit form (`ae_idaa_comp__event_obj_id_edit.svelte`) is organized into these sections.
@@ -604,18 +627,36 @@ Stores Novi auth context and per-submodule query settings:
novi_jitsi_mod_li: string[] // Jitsi moderator UUIDs
archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_obj }
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj }
recovery_meetings: { qry__fulltext_str, qry__physical, qry__virtual, qry__type, qry__limit, edit__event_obj }
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj,
qry__enabled, qry__hidden, qry__limit, qry__offset, qry__order_by, qry__order_by_li }
recovery_meetings: {
qry__enabled, qry__hidden, qry__limit, qry__offset,
qry__fulltext_str, qry__physical, qry__virtual, qry__type,
qry__order_by, qry__order_by_li,
qry__favorites_only, // true = show only starred meetings (My Meetings filter)
edit__event_obj // null or event_id string when edit form is open
}
}
```
### `idaa_sess` (sessionStorage — cleared on tab close)
### `idaa_sess` (in-memory only — resets on page load)
UI state per submodule:
```typescript
{
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id, obj_changed }
bb: { qry__status, show__modal_edit__post_id, show__modal_view__post_id, obj_changed }
recovery_meetings: { qry__status, show__modal_edit, show__modal_view, attend_platform, obj_changed }
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id,
show__modal_edit__archive_content_id, show__modal_view__archive_content_id, obj_changed }
bb: { qry__status, edit__post_obj, show__inline_edit__post_obj, show__modal_edit__post_id,
show__modal_view__post_id, obj_changed }
recovery_meetings: {
qry__status, // null | 'loading' | 'done' | 'error'
qry__fulltext_str, // session-only copy (separate from persisted loc copy)
search_version, // incremented to trigger a new search cycle
edit__event_obj, // null | event_id — controls edit form visibility
show__modal_edit, show__modal_view,
show__modal_edit__event_id, show__modal_view__event_id,
attend_platform, // 'Zoom' | 'Jitsi' | null — platform selected in virtual attend section
obj_changed
}
}
```
@@ -780,4 +821,4 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
---
**Document Status:** ✅ Current
**Last Verified:** 2026-05-06added Module 5: Jitsi Reports (grouped view, UUID exclusion, known-meeting whitelist, UUID-based unique counts); fixed route tree (`jitsi_reports/` is inside `(idaa)/`)
**Last Verified:** 2026-05-18Recovery Meetings: added My Meetings / favorites (data_store-backed, star button on list + detail page), guided empty state for filtered zero-results, corrected filter/sort descriptions; updated `idaa_loc` and `idaa_sess` store schemas to match actual fields

View File

@@ -160,97 +160,74 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
</button>
</div>
<!-- Filter chips: Location + Type -->
<div
class="border-surface-300-700 flex w-full max-w-xl flex-row flex-wrap items-center justify-start gap-2 border-b p-1">
<span class="form-check-label d-inline-block text-xs">
Location?
</span>
<!-- <div class="ae_row ae_flex_justify_around ae_width_md"> -->
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold">
<span> Virtual </span>
<input
name="qry_virtual"
type="checkbox"
bind:checked={$idaa_loc.recovery_meetings.qry__virtual}
class="checkbox form-check-input d-inline-block inline" />
</label>
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold">
In-person
<input
name="qry_physical"
type="checkbox"
bind:checked={$idaa_loc.recovery_meetings.qry__physical}
class="checkbox form-check-input d-inline-block inline" />
</label>
<!-- </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">
<div
class="border-surface-300-700 flex w-full max-w-xl flex-row flex-wrap items-center justify-start gap-2 border-b p-1">
<span class="form-check-label d-inline-block text-xs"> Type? </span>
<!-- Location: independent toggles (a meeting can be both virtual and in-person) -->
<div class="flex flex-row flex-wrap items-center gap-1">
<span class="text-xs opacity-50">Location:</span>
<button
type="button"
onclick={() => { $idaa_loc.recovery_meetings.qry__virtual = !$idaa_loc.recovery_meetings.qry__virtual; }}
aria-pressed={$idaa_loc.recovery_meetings.qry__virtual}
title="Show virtual / online meetings"
class="novi_btn btn btn-sm transition-all
{$idaa_loc.recovery_meetings.qry__virtual
? 'preset-filled-tertiary-400-600'
: 'preset-outlined-surface-300-700 opacity-60 hover:opacity-100'}">
<span class="fas fa-laptop mr-1"></span>Virtual
</button>
<button
type="button"
onclick={() => { $idaa_loc.recovery_meetings.qry__physical = !$idaa_loc.recovery_meetings.qry__physical; }}
aria-pressed={$idaa_loc.recovery_meetings.qry__physical}
title="Show in-person / face-to-face meetings"
class="novi_btn btn btn-sm transition-all
{$idaa_loc.recovery_meetings.qry__physical
? 'preset-filled-tertiary-400-600'
: 'preset-outlined-surface-300-700 opacity-60 hover:opacity-100'}">
<span class="fas fa-home mr-1"></span>In-Person
</button>
</div>
<!-- <div class="ae_row ae_flex_justify_around ae_width_100"> -->
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold">
<span> All </span>
<input
name="qry_type"
type="radio"
value=""
bind:group={$idaa_loc.recovery_meetings.qry__type}
class="radio form-check-input d-inline-block inline"
title="Show all meeting types" />
</label>
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold">
IDAA
<input
name="qry_type"
type="radio"
value="IDAA"
bind:group={$idaa_loc.recovery_meetings.qry__type}
class="radio form-check-input d-inline-block inline"
title="Open to IDAA members only" />
</label>
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold">
Caduceus
<input
name="qry_type"
type="radio"
value="Caduceus"
bind:group={$idaa_loc.recovery_meetings.qry__type}
class="radio form-check-input d-inline-block inline"
title="Open to all healthcare workers including those who do not qualify for IDAA" />
</label>
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold">
Family Recovery
<input
name="qry_type"
type="radio"
value="Family Recovery"
bind:group={$idaa_loc.recovery_meetings.qry__type}
class="radio form-check-input d-inline-block inline"
title="Open to spouses, parents, and children of medical professionals who have substance use disorder." />
</label>
<span class="ml-auto"></span>
<!-- Type: mutually exclusive; clicking the active chip deselects (goes back to All) -->
<div class="flex flex-row flex-wrap items-center gap-1">
<span class="text-xs opacity-50">Type:</span>
{#each [
['', 'All', 'Show all meeting types'],
['IDAA', 'IDAA', 'Open to IDAA members only'],
['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']
] as [val, label, tip]}
{@const active = val === '' ? !$idaa_loc.recovery_meetings.qry__type : $idaa_loc.recovery_meetings.qry__type === val}
<button
type="button"
onclick={() => {
$idaa_loc.recovery_meetings.qry__type =
(val !== '' && $idaa_loc.recovery_meetings.qry__type === val) ? '' : val;
}}
aria-pressed={active}
title={tip}
class="novi_btn btn btn-sm transition-all
{active
? 'preset-filled-secondary-400-600'
: 'preset-outlined-surface-300-700 opacity-60 hover:opacity-100'}">
{label}
</button>
{/each}
</div>
{#if $ae_loc.edit_mode}
<label
class="legend form-check-label d-inline-block inline w-auto flex-row items-center justify-center gap-1 text-sm font-semibold"
title="When enabled, search results are fetched directly from the server first. When disabled, local results are shown instantly while revalidating in the background.">
<span class="text-xs"> Remote First? </span>
class="flex flex-row items-center gap-1 text-xs opacity-60 cursor-pointer"
title="When enabled, search results are fetched directly from the server first. Disable for instant local results with background revalidation.">
<input
name="qry_remote_first"
type="checkbox"
bind:checked={
$idaa_loc.recovery_meetings.qry__remote_first
}
class="checkbox form-check-input d-inline-block inline" />
bind:checked={$idaa_loc.recovery_meetings.qry__remote_first}
class="checkbox form-check-input d-inline-block" />
Remote First
</label>
{/if}
</div>