Compare commits
13 Commits
ab9e54d768
...
bbab9e7c8c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbab9e7c8c | ||
|
|
daf1570781 | ||
|
|
c69e40829f | ||
|
|
429f38996a | ||
|
|
6857f1226c | ||
|
|
bb84117991 | ||
|
|
7a1099bbbe | ||
|
|
3a81887c56 | ||
|
|
730fb19d60 | ||
|
|
b32fb05138 | ||
|
|
12429ccf2e | ||
|
|
2d552b36fd | ||
|
|
3ed1a2a6c4 |
569
documentation/AE__UI_UX_future_ideas.md
Normal file
569
documentation/AE__UI_UX_future_ideas.md
Normal file
@@ -0,0 +1,569 @@
|
||||
# Aether UI/UX — Future Ideas
|
||||
|
||||
> Collection of concrete UX improvements for the Aether frontend. Each entry includes
|
||||
> the rationale, current behavior, proposed change, and implementation notes.
|
||||
> **Date:** 2026-05-17
|
||||
|
||||
---
|
||||
|
||||
## IDAA Recovery Meetings
|
||||
|
||||
### 1. Guided empty state with active filters — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** When filters return 0 results, the page shows:
|
||||
"No recovery meetings found matching your criteria."
|
||||
The member has no indication whether this is a bug, genuinely no data, or just
|
||||
overly narrow filters.
|
||||
|
||||
**Proposed change:** When filters are active AND the result count is 0, show a
|
||||
helpful prompt instead of the bare message:
|
||||
|
||||
```
|
||||
No meetings found for these filters.
|
||||
Try broadening your search or [Clear all filters →]
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `has_active_filters` derived in `+page.svelte` that checks whether any of
|
||||
`qry__physical`, `qry__virtual`, `qry__type`, or `qry__fulltext_str` is set.
|
||||
- In the template's `{:else}` block (line ~443), branch on `has_active_filters`:
|
||||
- `true` → show the guided message + "Clear Filters" button
|
||||
- `false` → show the existing escape-hatch flow (timed "Refresh Meeting Cache" button
|
||||
after 8 seconds, since zero unfiltered results always indicates a problem)
|
||||
- The "Clear Filters" button resets all four filter fields to `null`/`''` and bumps
|
||||
`search_version` to trigger a fresh unfiltered search.
|
||||
- Distinct from the `error` state — this is a successful search (`qry__status === 'done'`)
|
||||
with an empty result set.
|
||||
|
||||
**Implemented:** `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. `has_active_filters`
|
||||
derived checks `qry__physical`, `qry__virtual`, `qry__type`, and `qry__fulltext_str`. Empty
|
||||
state branches on `has_active_filters`: active filters → guided message + "Clear Filters"
|
||||
button; no active filters → existing escape-hatch flow (timed "Refresh Meeting Cache" after
|
||||
8 seconds).
|
||||
|
||||
---
|
||||
|
||||
### 2. Quick-filter chips below the search bar — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** Members toggle filters via small checkboxes (Virtual, In-person)
|
||||
and radio buttons (All, IDAA, Caduceus, Family Recovery). These require precise
|
||||
mouse/tap targeting and scanning several lines of filter UI to discover and use.
|
||||
|
||||
**Proposed change:** Add a row of preset chip buttons directly below the search input:
|
||||
|
||||
```
|
||||
[🖥 Virtual] [🏠 In-Person] [🩺 IDAA] [Caduceus] [Family Recovery] [All Types]
|
||||
```
|
||||
|
||||
- Each chip toggles the corresponding filter (`qry__virtual`, `qry__physical`, `qry__type`)
|
||||
and triggers an immediate search.
|
||||
- Selected chips get a filled/pressed style; unselected chips are outlined.
|
||||
- "All Types" is the default selected state (no type filter). Clicking another type
|
||||
chip deselects "All Types" (radio behavior for the type dimension). Virtual and
|
||||
In-person are independent toggles (checkbox behavior — can select both).
|
||||
- The existing checkboxes/radio buttons remain as the underlying state storage
|
||||
(`$idaa_loc.recovery_meetings.*`). The chips are a convenience layer — they write
|
||||
to the same store fields and call `handle_search_trigger()`.
|
||||
|
||||
**Implementation notes:**
|
||||
- Place in `ae_idaa_comp__event_obj_qry.svelte` between the search input row and the
|
||||
current filter rows.
|
||||
- Optionally hide the existing checkbox/radio filter rows when the chips are present
|
||||
(or keep both — the checkboxes serve as accessible form controls; the chips are
|
||||
the primary visual interaction).
|
||||
- On mobile, chips wrap to a second row naturally with `flex-wrap`.
|
||||
|
||||
**Implemented:** `ae_idaa_comp__event_obj_qry.svelte`. Chips replaced the old
|
||||
checkbox/radio/select UI entirely rather than layering on top. Two chip rows:
|
||||
Row 1 — My Meetings (first), Virtual, In-Person. Row 2 — All / IDAA / Caduceus /
|
||||
Family Recovery type chips. Cycling sort button replaces separate sort options
|
||||
(see item below). Max Results uses a +/− stepper. Sort and max are in a third
|
||||
row below the chips, inside the same `<form>` constraint.
|
||||
|
||||
---
|
||||
|
||||
### 3. Language: "Searching..." vs "Loading..."
|
||||
|
||||
**Current behavior:** The loading state always shows the same message:
|
||||
|
||||
```
|
||||
🔄 Searching...
|
||||
```
|
||||
|
||||
This appears on initial page load (when the user hasn't typed anything) and after
|
||||
the user clicks Search or toggles a filter. The word "Searching" implies the user
|
||||
initiated a search, which is misleading on initial page load — it's a cold cache
|
||||
load, not an active search.
|
||||
|
||||
**Proposed change:** Distinguish the two loading contexts:
|
||||
|
||||
| Context | Message |
|
||||
|---------|---------|
|
||||
| Initial page load (no filters, no search text) | "Loading meetings..." |
|
||||
| User clicked Search or toggled a filter | "Searching..." (keep current) |
|
||||
|
||||
**Implementation notes:**
|
||||
- In `+page.svelte` template around line 422, check whether `qry__fulltext_str` is
|
||||
empty AND no filter checkboxes/radios are active. If so, show "Loading meetings...";
|
||||
otherwise show "Searching...".
|
||||
- This is purely a label change — no logic changes needed. The condition can be the
|
||||
same `has_active_filters` derived from item #1.
|
||||
- Also update the list component's standalone loading state in
|
||||
`ae_idaa_comp__event_obj_li.svelte` line 556-558 to use the same distinction.
|
||||
|
||||
---
|
||||
|
||||
### 4. Filter row collapsing on mobile
|
||||
|
||||
**Current behavior:** The query bar has three filter rows (Location checkboxes,
|
||||
Type radios, Max/Sort selects) plus the search input row and the action button row.
|
||||
Combined, this takes roughly 200px of vertical space. On mobile — especially inside
|
||||
the Novi iframe on a phone — meeting cards are pushed below the fold.
|
||||
|
||||
**Proposed change:** On viewports below `md` (768px), collapse the Location and Type
|
||||
filter rows behind a "Filters ▾" toggle. The Max Results and Sort selects stay visible
|
||||
since they're used frequently. The action buttons (Show Hidden, Create Meeting, Export)
|
||||
move inside the collapsed panel or stay visible based on available width.
|
||||
|
||||
```
|
||||
[Search input............................] [Search]
|
||||
|
||||
[Filters ▾] [Max: 150 ▾] [Sort: Last Updated ▾]
|
||||
```
|
||||
|
||||
Clicking "Filters ▾" expands the panel with Location checkboxes and Type radios.
|
||||
|
||||
**Implementation notes:**
|
||||
- Use a `$state` boolean `show_filters` (session-only, resets on page load).
|
||||
- Wrap the filter rows in a `{#if show_filters}` block.
|
||||
- Persist in `$idaa_sess.recovery_meetings.show_filters_expanded` if you want the
|
||||
state to survive navigation within the module (same tab session).
|
||||
- The Tailwind `md:` breakpoint works for the collapse trigger: `class:hidden={!show_filters}`
|
||||
combined with `class:md:block` to always show on desktop.
|
||||
- Test inside the Novi iframe — Bootstrap v3 may add its own `hidden` behavior on
|
||||
`md` breakpoints that conflicts with Tailwind's.
|
||||
|
||||
---
|
||||
|
||||
### 5. Human-readable schedule line on cards
|
||||
|
||||
**Current behavior:** The meeting card displays weekdays as a flat, dense span list:
|
||||
|
||||
```
|
||||
Sunday Monday Wednesday Friday
|
||||
```
|
||||
|
||||
The timezone is shown separately as `(America/Chicago)`, and the start time is in
|
||||
a compact `7:00 PM` format. These three pieces of information are visually separated
|
||||
and require the member to mentally assemble the schedule.
|
||||
|
||||
**Proposed change:** Render a computed one-liner that combines them:
|
||||
|
||||
```
|
||||
🕐 Mondays, Wednesdays, Fridays at 7:00 PM CT
|
||||
```
|
||||
|
||||
- Weekday names are built from the `weekday_*` booleans on the event object.
|
||||
- "Mondays, Wednesdays" uses the range-joining convention (comma-separated, "and"
|
||||
before the last item for two days; "Mondays through Fridays" for consecutive spans
|
||||
of 3+ days).
|
||||
- Timezone abbreviation is extracted from `timezone` (e.g., `America/Chicago` → `CT`,
|
||||
`America/New_York` → `ET`). A small lookup table handles the common ones; fall back
|
||||
to the raw timezone string for unknown values.
|
||||
- If `timezone` is null/missing, fall back to the current flat display — don't
|
||||
silently drop information.
|
||||
- Today's meetings could optionally get a subtle "Today" badge or highlight (extra
|
||||
polish, not required for the initial version).
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$derived` in `ae_idaa_comp__event_obj_li.svelte` that computes the schedule
|
||||
string from the event object's `weekday_*` fields, `recurring_start_time`, and
|
||||
`timezone`.
|
||||
- Helper function in `ae_util` for the weekday list → natural language string
|
||||
(e.g., `['Monday', 'Wednesday', 'Friday']` → `"Mondays, Wednesdays, and Fridays"`).
|
||||
- Helper function or small lookup for timezone → abbreviation.
|
||||
- Fall back to the current flat display when `timezone` is missing to avoid losing
|
||||
information.
|
||||
|
||||
---
|
||||
|
||||
### 6. Show result count during search, not just after
|
||||
|
||||
**Current behavior:** The result count badge ("Results: 25") only appears inside the
|
||||
list wrapper component (`ae_idaa_comp__event_obj_li.svelte` line 98-108) when the
|
||||
visible result list is non-empty. During loading, the user sees only a spinner with
|
||||
no indication of how many meetings exist or what the search is operating on.
|
||||
|
||||
**Proposed change:** Show a result count line at the page level (in `+page.svelte`)
|
||||
that is always visible once the first search completes:
|
||||
|
||||
```
|
||||
25 of 140 meetings ← after search completes, with result count + total
|
||||
Searching 140 meetings... ← during initial load (cold cache, no prior result)
|
||||
0 results for these filters ← empty but filters are active (ties into item #1)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Lift the count display from the list component to `+page.svelte`, placed between
|
||||
the query bar (`Comp__event_obj_qry`) and the list wrapper.
|
||||
- The total count is available from the IDB fast path: after the initial unfiltered
|
||||
search populates `db_events.event`, the total is `db_events.event.count()` (or
|
||||
the count of records matching `account_id`).
|
||||
- The visible count is `event_id_li.length` after search completes.
|
||||
- Store the last known total in a `$state` variable so it persists across searches
|
||||
(the total changes infrequently). Refresh the total on the first search after
|
||||
page load.
|
||||
- Format: `{visible} of {total} meetings` when filters/search are active;
|
||||
`{visible} meetings` when browsing all (no active filters).
|
||||
- During loading with no prior results: show "Loading meetings..." (from item #3)
|
||||
rather than a count.
|
||||
|
||||
---
|
||||
|
||||
### 7. "Live Now" and "Starting Soon" indicators
|
||||
|
||||
**Current behavior:** Meetings are shown in a static list. To find one happening
|
||||
now, a member must scan the "When" line of multiple cards and compare the time
|
||||
to their own clock.
|
||||
|
||||
**Proposed change:** Add a high-visibility badge or pulse indicator for meetings
|
||||
that are currently in progress or starting in the next 15 minutes.
|
||||
|
||||
- "LIVE NOW" (Green pulse badge) → if `current_time` is within `[start, start + 1 hour]`.
|
||||
- "STARTING SOON" (Yellow badge) → if `current_time` is within `[start - 15 min, start]`.
|
||||
- On the card, move the "Join Zoom" or "Join Jitsi" button to the very top or
|
||||
make it significantly larger when the meeting is live.
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$derived` state `is_live` and `is_starting_soon` to the card component.
|
||||
- Requires calculating "current time in meeting's timezone" using `Temporal` or
|
||||
a date helper.
|
||||
- Ensure the pulse animation is subtle and respects `prefers-reduced-motion`.
|
||||
|
||||
---
|
||||
|
||||
### 8. Local Timezone Conversion
|
||||
|
||||
**Current behavior:** Meetings show their native timezone (e.g., "7:00 PM America/Chicago").
|
||||
The "Your TZ" line is currently a placeholder and doesn't perform conversion.
|
||||
|
||||
**Proposed change:** Automatically detect the member's browser timezone and
|
||||
show the converted time if it differs from the meeting's native timezone.
|
||||
|
||||
```
|
||||
🕐 7:00 PM CT (8:00 PM ET your time)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Use `Intl.DateTimeFormat().resolvedOptions().timeZone` to get the user's TZ.
|
||||
- If `user_tz !== meeting_tz`, perform the conversion.
|
||||
- If the conversion results in a different day (e.g., late night ET vs early morning Europe),
|
||||
prefix with "Tomorrow at..." or "Yesterday at...".
|
||||
|
||||
---
|
||||
|
||||
### 9. Favorites / "My Meetings" — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** Members scan the full list every time they want to find
|
||||
their regular weekly meeting.
|
||||
|
||||
**Proposed change:** Add a "Star" icon to every meeting card.
|
||||
- Starring a meeting adds it to a `favorites` list stored in `$idaa_loc`.
|
||||
- Favorited meetings are pinned to the top of the list by default, regardless
|
||||
of other sort orders.
|
||||
- Add a "Favorites" filter toggle in the query bar to show *only* starred meetings.
|
||||
|
||||
**Implementation notes:**
|
||||
- Store as an array of `event_id` strings in `$idaa_loc.recovery_meetings.favorites`.
|
||||
- Update the `visible_event_obj_li` derived in `+page.svelte` to prioritize
|
||||
these IDs in the sort logic.
|
||||
|
||||
**Implemented:** Star toggle on the `[event_id]` detail page
|
||||
(`src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte`).
|
||||
"My Meetings" filter chip is first in the filter chip row on the list page.
|
||||
**Implementation differs from proposal:** favorites stored server-side in a
|
||||
`data_store` record (code: `idaa_meetings_favorites`) as a UUID-keyed JSON map
|
||||
rather than in `$idaa_loc` — this means favorites persist across browsers and
|
||||
devices without Novi write capability. Pinning favorites to the top of the list
|
||||
was not implemented; the filter chip shows only favorites instead.
|
||||
|
||||
---
|
||||
|
||||
### 10. "Add to Calendar" (iCal / Google)
|
||||
|
||||
**Current behavior:** Members must manually create calendar events if they
|
||||
want reminders for recurring meetings.
|
||||
|
||||
**Proposed change:** Add an "Add to Calendar" dropdown button on the meeting
|
||||
detail page (and optionally the card).
|
||||
- Generates a `.ics` file or a Google Calendar URL with the recurring rule
|
||||
(e.g., "Every Wednesday at 7pm").
|
||||
- Includes the meeting name, description, and the Zoom/Jitsi link in the location field.
|
||||
|
||||
**Implementation notes:**
|
||||
- Use a helper to generate RFC 5545 `RRULE` strings from the `weekday_*` and
|
||||
`recurring_pattern` fields.
|
||||
- Include the `attend_url` in the calendar event description for one-tap join
|
||||
from phone lock screens.
|
||||
|
||||
---
|
||||
|
||||
### 11. Geographic Search for In-Person Meetings
|
||||
|
||||
**Current behavior:** The only location filter is a binary "Physical" checkbox.
|
||||
Members must use fulltext search (e.g., "Chicago") to find local meetings.
|
||||
|
||||
**Proposed change:** Add a "City/State" search input or a map view.
|
||||
- When `Physical` is checked, show a "Near [City, State]" input.
|
||||
- Map view (optional): A toggle to switch from "List" to "Map" view, plotting
|
||||
meetings on a map using their `location_address_json` coordinates.
|
||||
|
||||
**Implementation notes:**
|
||||
- The `event` table has `location_address_json` which often contains city/state.
|
||||
- Simple implementation: a city-picker dropdown populated from the distinct
|
||||
`location_address_json->>'$.city'` values in the current result set.
|
||||
|
||||
---
|
||||
|
||||
### 12. Prominent "Join" button for virtual meetings
|
||||
|
||||
**Current behavior:** On each meeting card, the Zoom or Jitsi join link is rendered
|
||||
as a small `btn-sm` inside the content area, visually equivalent to other label/value
|
||||
rows. The "Meeting Details" button at the top of the card is rendered *larger* than the
|
||||
join link — meaning the primary action for a member who wants to attend a meeting right
|
||||
now is visually subordinate to a navigation link.
|
||||
|
||||
The copy-to-clipboard button for the join link is gated behind `$ae_loc.manager_access`,
|
||||
so regular members have no easy way to share the link with a sponsee.
|
||||
|
||||
**Proposed change:** For virtual meetings, elevate the join button to a full-width
|
||||
prominent CTA inside the card header area, directly below the meeting name and badges:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 📅 Monday Night IDAA Discussion 🖥 Virtual │
|
||||
│ │
|
||||
│ [ 🎥 Join Zoom Meeting ] ← full-width │
|
||||
│ [ 📋 Meeting Details ] │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- The Join button uses `preset-filled` (solid) styling; Meeting Details uses
|
||||
`preset-outlined` (hollow). This makes the action hierarchy visually clear.
|
||||
- On mobile especially, a full-width join button is much easier to tap than a
|
||||
small inline link buried inside label rows.
|
||||
- Replace manager-only clipboard with a Web Share API button for all members on
|
||||
virtual meetings: `navigator.share({ title, url })` on mobile triggers the native
|
||||
OS share sheet. Fall back to clipboard copy on desktop (where `navigator.share`
|
||||
is often unavailable). This lets members easily send a meeting link to a sponsee.
|
||||
- Passcode, if present, moves to the Meeting Details page — exposing it in the
|
||||
list view is unnecessary and clutters the card.
|
||||
|
||||
**Implementation notes:**
|
||||
- In `ae_idaa_comp__event_obj_li.svelte`, move the Zoom/Jitsi attend block from
|
||||
the `event__content` section up into the `ae_options` div (line ~200), rendered
|
||||
only when `idaa_event_obj?.virtual` is true and an attend URL exists.
|
||||
- Keep the existing small label/link in `event__content` as a fallback for when
|
||||
the prominent button is not shown (non-virtual meetings may still have a URL).
|
||||
- Web Share: `{#if navigator?.share}` guard; wrap in a try/catch (user cancels
|
||||
the share sheet throws `AbortError`).
|
||||
- The Live Now / Starting Soon badges from item #7, when implemented, should also
|
||||
interact with this button — e.g., a pulsing green border when the meeting is live.
|
||||
|
||||
---
|
||||
|
||||
### 13. "Today's Meetings" section at the top of the list
|
||||
|
||||
**Current behavior:** The meeting list shows all results sorted by the selected sort
|
||||
order. To find a meeting happening today, a member must scan every card's "When" line
|
||||
and mentally compare it to the current day and time. There is no at-a-glance view
|
||||
of what's available right now or later today.
|
||||
|
||||
This is distinct from the "Live Now" badge in item #7 (which marks individual cards
|
||||
after they're already displayed in a long list). This is a dedicated section pinned
|
||||
above the main results.
|
||||
|
||||
**Proposed change:** Add a collapsible "Today" section at the very top of the results
|
||||
list that shows only meetings scheduled on the current day of the week, sorted by
|
||||
start time:
|
||||
|
||||
```
|
||||
▼ Today — Sunday, May 17 3 meetings
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Sunday Serenity Discussion 7:00 AM ET 🔴Live │
|
||||
│ IDAA Sunday Big Book 2:00 PM CT │
|
||||
│ Sunday Night IDAA 8:00 PM ET │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
All Meetings (140)
|
||||
...
|
||||
```
|
||||
|
||||
- The section is collapsed by default if it's empty (no meetings today).
|
||||
- Meetings in the "Today" section also appear in the main list below — this is a
|
||||
quick-access shortcut, not a filter.
|
||||
- Past meetings (start time has already passed today) are dimmed but still shown;
|
||||
a meeting may still be in progress.
|
||||
|
||||
**Implementation notes:**
|
||||
- Compute current day-of-week in the browser: `new Date().getDay()` → 0=Sunday, 6=Saturday.
|
||||
Map to the `weekday_*` boolean fields on the event object (e.g., day 0 → `weekday_sunday`).
|
||||
- Filter `visible_event_obj_li` (already computed in the list wrapper) for items where
|
||||
the matching `weekday_*` field is truthy. Sort by `recurring_start_time`.
|
||||
- Store collapse state in `$idaa_sess.recovery_meetings.today_section_expanded` (session
|
||||
only; default true so it's visible on first load).
|
||||
- Renders correctly in the Novi iframe since it's just a filtered sub-list of existing
|
||||
data — no additional API calls needed.
|
||||
- If item #7 (Live Now) is implemented, the "Today" section naturally becomes the host
|
||||
for the live/starting-soon badges, since that's where members will look first.
|
||||
|
||||
---
|
||||
|
||||
### 14. Data freshness indicator *(low priority — deprioritized)*
|
||||
|
||||
**Note:** Meeting records change infrequently — once established, a meeting's schedule,
|
||||
type, and contact info are typically stable for months or years. The occasional update is
|
||||
usually minor wording. Surfacing a freshness indicator for data this static would add
|
||||
visual noise with very little member benefit. The existing error state (item #4 in the
|
||||
bug fix, distinct "Unable to load meetings") and the escape-hatch cache-reset button
|
||||
already handle the reliability-concern case. This idea is recorded for completeness but
|
||||
is not recommended for implementation.
|
||||
|
||||
---
|
||||
|
||||
### 15. "Confirmed meetings only" default filter
|
||||
|
||||
**Current behavior:** All meetings are shown by default, including those with
|
||||
`status === 'unknown'` (not yet confirmed by IDAA Central Office). The "Not Confirmed
|
||||
by IDAA" warning badge on those cards is alarming-looking but does nothing to prevent
|
||||
unverified meetings from dominating the list.
|
||||
|
||||
There is currently no filter to hide unconfirmed meetings. Members have no choice but
|
||||
to see them all.
|
||||
|
||||
**Business rationale:** IDAA staff want meeting chairs to submit their meeting info for
|
||||
verification. Defaulting to "confirmed only" creates a natural incentive: unconfirmed
|
||||
meetings disappear from the default member view, which encourages chairs to contact
|
||||
IDAA staff and get their meeting verified. It also gives members a cleaner, higher-
|
||||
confidence list by default — they're not seeing meetings that may be outdated or
|
||||
inactive.
|
||||
|
||||
**Proposed change:** Add a `qry__confirmed` filter field with three states:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `'confirmed_only'` (default) | Hide meetings where `status === 'unknown'` |
|
||||
| `'all'` | Show all meetings, confirmed and unconfirmed |
|
||||
| `'unconfirmed_only'` | Show only unconfirmed (admin/staff use) |
|
||||
|
||||
The filter UI shows a simple toggle in the query bar:
|
||||
```
|
||||
[✓ Confirmed Only] ← default, shown as active chip or checkbox
|
||||
```
|
||||
|
||||
When the member switches to "All", the unconfirmed meetings appear with their warning
|
||||
badge (see item #15 for making that badge useful on mobile).
|
||||
|
||||
For trusted/admin users, the default should remain `'all'` so staff can see the full
|
||||
picture without having to change a setting.
|
||||
|
||||
**Implementation notes:**
|
||||
- Add `qry__confirmed: 'confirmed_only' | 'all' | 'unconfirmed_only'` to
|
||||
`$idaa_loc.recovery_meetings` defaults, defaulting to `'confirmed_only'`.
|
||||
Trusted users default to `'all'`.
|
||||
- Apply the filter in both the IDB fast path (`db_events.event.filter()`) and the
|
||||
API revalidation secondary filter in `handle_search_refresh`. IDB: check
|
||||
`ev.status !== 'unknown'` when `qry__confirmed === 'confirmed_only'`. API: same
|
||||
post-fetch client-side filter.
|
||||
- Pass `qry__confirmed` to `events_func.search__event` if the API supports a
|
||||
`status` filter param; otherwise handle it client-side only.
|
||||
- The `no_results_no_filters` derived (used for the escape-hatch button) should NOT
|
||||
treat `qry__confirmed === 'confirmed_only'` as an active filter — it's the default
|
||||
state, not a narrowing choice the member made. Only count it as a filter if the
|
||||
member explicitly switched it to `'all'` or `'unconfirmed_only'`.
|
||||
- Add a count badge to the toggle: "Confirmed Only (132 of 140)" so members can see
|
||||
how many unconfirmed meetings exist without having to switch the filter.
|
||||
|
||||
---
|
||||
|
||||
### 16. "Not Confirmed" status — inline explanation on mobile
|
||||
|
||||
**Current behavior:** Meetings with `status === 'unknown'` show a warning badge:
|
||||
`⚠ Not Confirmed by IDAA ⚠`. The badge has a `title` attribute with a full explanation
|
||||
(~2 sentences). Title tooltips are invisible on mobile — tapping the badge does nothing.
|
||||
Members on phones see a alarming-looking warning with no explanation of what it means
|
||||
or what they should do.
|
||||
|
||||
**Proposed change:** Make the badge tappable. On tap (or hover on desktop), show an
|
||||
inline explanation panel directly below the badge:
|
||||
|
||||
```
|
||||
⚠ Not Confirmed by IDAA [?]
|
||||
|
||||
↓ (on tap)
|
||||
|
||||
This meeting has not been confirmed by IDAA Central Office.
|
||||
Please reach out to the chair for current information.
|
||||
If this meeting is active, email info@idaa.org to confirm it.
|
||||
[✕ Close]
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$state show_unconfirmed_info = false` per card (scoped to the `{#each}` block).
|
||||
- Replace the `title` attribute with an `onclick` toggle that sets `show_unconfirmed_info`.
|
||||
- The explanation renders in a `{#if show_unconfirmed_info}` block directly below the
|
||||
badge row — a simple div with a rounded border and the existing tooltip text.
|
||||
- The `mailto:info@idaa.org` link in the explanation is already in the tooltip text;
|
||||
making it a real clickable link here rather than plain text in a tooltip is a direct
|
||||
improvement for mobile members who want to report a confirmed meeting.
|
||||
- This pattern also applies to other `title`-only tooltips on the page if they appear.
|
||||
- Note: with item #15 defaulting to "confirmed only", most members will never encounter
|
||||
this badge unless they switch to the "All" view. The inline explanation is still worth
|
||||
implementing for that audience, but the default filter reduces how often it's seen.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
### 17. Cycling sort button — ✅ Implemented 2026-05-18
|
||||
|
||||
**Problem:** Three separate sort chip buttons (Last Updated / Name A→Z / Name Z→A) took
|
||||
too much horizontal space and caused layout bounce as the selected chip changed width.
|
||||
|
||||
**Implemented:** Single cycling button in `ae_idaa_comp__event_obj_qry.svelte`.
|
||||
Clicking advances through `sort_modes` array (Last Updated → Name A→Z → Name Z→A → repeat)
|
||||
using `$derived` index + `cycle_sort()` function. Button has `min-w-36` to prevent bounce.
|
||||
Icon changes per mode (fa-clock / fa-sort-alpha-down / fa-sort-alpha-up-alt). A small
|
||||
fa-redo icon indicates it's a cycling control.
|
||||
|
||||
---
|
||||
|
||||
### 18. Collapsible "Meeting Info" data store panel — ✅ Implemented 2026-05-18
|
||||
|
||||
**Problem:** The `Element_data_store` panel (code: `recovery_meetings_info`) displays
|
||||
between the filter bar and the meeting results list. Once a member has read it, it
|
||||
consumes vertical space on every page load and pushes results below the fold, especially
|
||||
in the Novi iframe on mobile.
|
||||
|
||||
**Implemented:** Toggle button wrapping the `<Element_data_store>` in
|
||||
`src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. Button shows
|
||||
"Meeting Info" with a chevron (up = expanded, down = collapsed). Collapse state
|
||||
persisted in `$idaa_loc.recovery_meetings.ds_info_collapsed` (localStorage) so the
|
||||
user's preference survives page reloads. New field added to `idaa_local_data_struct`
|
||||
in `ae_idaa_stores.ts` — no version bump needed (existing users without the field
|
||||
get `undefined` which is falsy = expanded, the correct default).
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The fulltext search (`qry__fulltext_str`) searches against the `default_qry_str`
|
||||
field, which is a server-side composite that already includes contact info, day-of-week
|
||||
text, meeting type, location, and other metadata. The placeholder text in the search
|
||||
input is accurate — it genuinely searches contacts and schedule information despite
|
||||
those fields being stored in separate columns.
|
||||
- All changes must render correctly inside the Novi iframe context (Bootstrap v3.4.1
|
||||
CSS conflicts — see `CLIENT__IDAA_and_customized_mods.md` for known issues).
|
||||
- Mobile testing should cover Android Chrome specifically — the original "no meetings
|
||||
found" bug disproportionately affected mobile users with intermittent connections.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Client:** International Doctors in Alcoholics Anonymous (IDAA)
|
||||
**Module Path:** `src/routes/idaa/`
|
||||
**State Stores:** `src/lib/stores/ae_idaa_stores.ts`
|
||||
**Last Updated:** 2026-03-09 (Novi UUID verification upgrade)
|
||||
**Last Updated:** 2026-05-18 (Default limit and stepper update)
|
||||
|
||||
---
|
||||
|
||||
@@ -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 A–Z, Meeting Name Z–A.
|
||||
|
||||
**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-06 — added 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-18 — Recovery Meetings: updated default limit to 100 and added 75 to limit stepper; 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
|
||||
|
||||
58
documentation/PROPOSAL__IDAA_UI_UX_Roadmap_2026.md
Normal file
58
documentation/PROPOSAL__IDAA_UI_UX_Roadmap_2026.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# IDAA Recovery Meetings: UI/UX Improvement Roadmap
|
||||
|
||||
This document outlines proposed enhancements for the IDAA Recovery Meeting module. The goal is to make it easier for members to find and attend meetings, especially on mobile devices, while providing IDAA staff with better tools to manage meeting data quality.
|
||||
|
||||
## 🏆 The "Big Wins" (Highest Member Impact)
|
||||
|
||||
### 1. Automatic Timezone Conversion
|
||||
* **The Problem:** Meetings currently show their "native" time (e.g., 7:00 PM Central). Members must manually calculate the time for their own location.
|
||||
* **The Fix:** The app will automatically detect the member's local timezone and show a converted time side-by-side (e.g., *"7:00 PM Central — 8:00 PM your time"*).
|
||||
|
||||
### 2. "Live Now" & "Today’s Meetings"
|
||||
* **The Fix:**
|
||||
* **Live Now:** A high-visibility green "LIVE" badge will pulse next to meetings currently in progress.
|
||||
* **Today’s Section:** A dedicated section at the very top of the list will show only meetings happening today, sorted by time, so members don't have to scroll through the full 140+ meeting list.
|
||||
|
||||
### 3. Clearer Meeting Schedules
|
||||
* **The Problem:** Days of the week are currently listed as a flat string (Sunday Monday Wednesday).
|
||||
* **The Fix:** Convert schedules into natural language one-liners: *"Mondays, Wednesdays, and Fridays at 7:00 PM."* This is much faster for the human eye to scan.
|
||||
|
||||
### 4. Favorites ("My Meetings")
|
||||
* **The Fix:** Members can "Star" their regular meetings. These favorites will be pinned to the top of their list for one-tap access every week.
|
||||
|
||||
### 5. "Add to Calendar"
|
||||
* **The Fix:** A button to automatically add a recurring meeting to a member’s Google, Apple, or Outlook calendar, including the Zoom/Jitsi link in the calendar event description.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Staff Tools & Data Quality
|
||||
|
||||
### 6. "Confirmed Only" Default View
|
||||
* **The Strategy:** To encourage meeting chairs to keep their information current, we propose defaulting the list to show **only** meetings confirmed by the Central Office.
|
||||
* **Member Benefit:** Higher confidence that the meeting they are about to join is active and the link is correct.
|
||||
* **Staff Benefit:** Creates a natural incentive for chairs to contact IDAA to get "Verified," as unverified meetings would require an extra click to see.
|
||||
|
||||
### 7. Mobile-Friendly "Not Confirmed" Explanations
|
||||
* **The Problem:** On mobile, the warning badge for unconfirmed meetings doesn't explain *why* it's there or *how* to fix it.
|
||||
* **The Fix:** Tapping the badge will show a simple popup: *"This meeting hasn't been verified recently. If you are the chair, please email info@idaa.org to confirm."*
|
||||
|
||||
---
|
||||
|
||||
## 📱 Ease-of-Use & Mobile Polishing
|
||||
|
||||
### 8. Prominent "Join" Buttons & Easy Sharing
|
||||
* **The Fix:** For virtual meetings, we will move the "Join Zoom" button to a prominent, full-width position at the top of the card. We will also add a "Share" button so members can easily text a meeting link to a sponsee.
|
||||
|
||||
### 9. Simplified "Quick-Filter" Chips
|
||||
* **The Fix:** Instead of small checkboxes, we will add large "Chips" (buttons) for common filters: `[🖥 Virtual]` `[🏠 In-Person]` `[🩺 IDAA]` `[Caduceus]`. These are much easier to tap on a phone screen.
|
||||
|
||||
### 10. Intelligent "No Results" Guidance
|
||||
* **The Problem:** If a member filters too narrowly (e.g., "Caduceus meetings in Hawaii on Tuesdays"), they just see a blank screen.
|
||||
* **The Fix:** A helpful prompt will appear: *"No meetings found for these filters. [Clear all filters →]"* to prevent members from thinking the app is broken.
|
||||
|
||||
---
|
||||
|
||||
### Next Steps
|
||||
1. **Feedback:** Staff identifies which 3–4 items are the highest priority for the next update.
|
||||
2. **Prototype:** We implement the high-priority items in the testing environment for staff review.
|
||||
3. **Deployment:** Changes are pushed live to the IDAA website.
|
||||
@@ -115,33 +115,40 @@ reactivity — only effects that actually read a changed field re-run.
|
||||
|
||||
---
|
||||
|
||||
### [Stores] IDB Content Version System (post June 10)
|
||||
Scaffold added to `store_versions.ts` (`IDB_CONTENT_VERSIONS` constant) — values defined but
|
||||
**not yet wired**. The mechanism mirrors `AE_LOC_VERSION` but targets Dexie table contents
|
||||
rather than localStorage keys.
|
||||
### [Stores] IDB Content Version System
|
||||
Scaffolded in `store_versions.ts` (`IDB_CONTENT_VERSIONS` constant + `check_and_clear_idb_table()`
|
||||
helper) and `core__idb_dexie.ts` (`check_and_clear_idb_tables()` batch helper). Mirrors
|
||||
`AE_LOC_VERSION` but targets Dexie table contents rather than localStorage keys.
|
||||
|
||||
**Why:** `db_save_ae_obj_li__ae_obj` uses a `properties_to_save` whitelist. When that whitelist
|
||||
changes (e.g. adding/removing a stored field), existing cached IDB records are stale but never
|
||||
automatically cleared. Users see the old shape until a record is individually refreshed.
|
||||
**Currently active:** `journals.journal_entry` (db_journals.ts), `events.event` (IDAA layout).
|
||||
All other tables are defined but not yet wired.
|
||||
|
||||
**How it will work:**
|
||||
- Each `db_*.ts` calls a helper (`core__idb_dexie.ts`) on open that checks a `_meta` IDB table
|
||||
- If stored version ≠ `IDB_CONTENT_VERSIONS[module][table]`, clear the table + update `_meta`
|
||||
- SWR repopulates from API on next access (same as any cold-start)
|
||||
**Real-world impact:** Stale IDB records from a `properties_to_save` change were the root cause
|
||||
of the IDAA Recovery Meetings "no meetings found" bug — a ~1-year unresolved issue (2025–2026).
|
||||
Fixed 2026-05-16 by wiring `events.event` into the IDAA layout and bumping its version to 2.
|
||||
See `BOOTSTRAP__AI_Agent_Quickstart.md` mistake #13 for the full postmortem.
|
||||
|
||||
**How it works:**
|
||||
- `check_and_clear_idb_table(db_table, 'module', 'table')` reads a localStorage key with the
|
||||
expected version from `IDB_CONTENT_VERSIONS`
|
||||
- On mismatch (or missing key), the Dexie table is cleared and the key is updated
|
||||
- SWR repopulates from API on next access — no explicit reload needed
|
||||
- Cost on version match: one `localStorage.getItem()` — effectively free
|
||||
- Bump a table's version in `IDB_CONTENT_VERSIONS` when `properties_to_save` changes shape
|
||||
|
||||
**IDAA consideration:**
|
||||
IDAA tables are already cleared by `indexedDB.deleteDatabase()` on sign-out/auth failure in
|
||||
`(idaa)/+layout.svelte`. The content version check is a *complementary* deploy-time reset, not
|
||||
a replacement. When wiring IDAA tables, ensure: (a) the check only runs on IDB open, not
|
||||
mid-session; (b) the `_meta` table is included in the `deleteDatabase()` wipe scope.
|
||||
a replacement.
|
||||
|
||||
**Tasks:**
|
||||
- [x] Write `check_and_clear_idb_tables()` helper in `core__idb_dexie.ts` (2026-05-14)
|
||||
- [x] Wire helper into `db_journals.ts` (pilot — `journal_entry: 2` clears stale content_md_html on first load) (2026-05-14)
|
||||
- [ ] Roll out to `db_events.ts`, `db_core.ts`
|
||||
- [x] Wire helper into `db_journals.ts` (pilot — `journal_entry: 2` cleared stale content_md_html) (2026-05-14)
|
||||
- [x] Wire `events.event` into IDAA layout `(idaa)/+layout.svelte` + bump version to 2 (2026-05-16)
|
||||
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, device, location, file)
|
||||
- [ ] Roll out to `db_core.ts` (site_domain, person, user)
|
||||
- [ ] Roll out to IDAA modules (`db_posts.ts`, `db_archives.ts`) — verify auth-wipe interaction first
|
||||
- [ ] Update `store_versions.ts` comment from "NOT YET ACTIVE" to document the active mechanism
|
||||
- [ ] Consolidate the two `check_and_clear_idb_table*` helpers (single-table in `store_versions.ts`, batch in `core__idb_dexie.ts`)
|
||||
|
||||
### [Stores] Refactor — Phase 2c (deferred)
|
||||
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
|
||||
@@ -225,6 +232,11 @@ suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a pac
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
|
||||
`contact_li_json` and include contact names/emails in the local text match check.
|
||||
|
||||
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
|
||||
The current search query can be taxing on the server. With ~150 active meetings, the view
|
||||
logic and supporting indexes need a performance review to ensure fast responses as the
|
||||
database grows. (Requested 2026-05-18)
|
||||
|
||||
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.**
|
||||
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
|
||||
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
|
||||
|
||||
@@ -84,7 +84,7 @@ const idaa_local_data_struct: key_val = {
|
||||
|
||||
qry__enabled: 'enabled', // all, disabled, enabled
|
||||
qry__hidden: 'not_hidden', // all, hidden, not_hidden
|
||||
qry__limit: 150,
|
||||
qry__limit: 100,
|
||||
qry__order_by: 'updated_on', // For the IDB index query; name, updated_on/created_on
|
||||
qry__order_by_li: {
|
||||
priority: 'DESC',
|
||||
@@ -98,7 +98,16 @@ const idaa_local_data_struct: key_val = {
|
||||
qry__fulltext_str: null,
|
||||
qry__physical: null,
|
||||
qry__type: null,
|
||||
qry__virtual: null
|
||||
qry__virtual: null,
|
||||
|
||||
// Favorites filter — when true, only show meetings the member has starred.
|
||||
// Favorites are stored server-side in event.mod_meetings_json.favorite (array of Novi UUIDs),
|
||||
// so they persist across browsers without requiring a Novi API write capability.
|
||||
qry__favorites_only: false,
|
||||
|
||||
// Collapse the "Meeting Info" data store panel between the search bar and results.
|
||||
// Persisted so the user's preference survives page reloads.
|
||||
ds_info_collapsed: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
export const AE_LOC_VERSION = 2; // Bumped 2026-03-30: force-clear stale site_cfg_json (novi_idaa_api_key missing bug)
|
||||
export const AE_EVENTS_LOC_VERSION = 1;
|
||||
export const AE_IDAA_LOC_VERSION = 1; // Added 2026-03-30: was missing, no wipe mechanism existed
|
||||
export const AE_IDAA_LOC_VERSION = 2; // Bumped 2026-05-18: change default qry__limit from 150 to 100
|
||||
export const AE_PRES_MGMT_LOC_VERSION = 1; // Added 2026-04-02: new standalone PersistedState store
|
||||
export const AE_BADGES_LOC_VERSION = 1; // Added 2026-04-02: promoted from events_loc.badges
|
||||
export const AE_LEADS_LOC_VERSION = 1; // Added 2026-04-03: promoted from events_loc.leads
|
||||
|
||||
@@ -114,7 +114,20 @@ $effect(() => {
|
||||
<section
|
||||
class="ae_section ae_meta post__meta mt-4 flex flex-row items-center justify-between gap-2 text-xs text-gray-500">
|
||||
<div class="ae_group flex flex-col gap-1">
|
||||
{#if $lq__post_obj?.anonymous}
|
||||
{#if $ae_loc.trusted_access && $lq__post_obj?.anonymous}
|
||||
<div class="post__posted_by">
|
||||
Posted by: <span class="fas fa-user-secret"></span>
|
||||
<span class="post__full_name">
|
||||
{$lq__post_obj?.full_name}
|
||||
*Anonymous*
|
||||
</span>
|
||||
{#if $ae_loc.trusted_access && $lq__post_obj?.email}
|
||||
<a
|
||||
href="mailto:{$lq__post_obj?.email}?subject=IDAA BB Post"
|
||||
>{$lq__post_obj?.email}</a>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $lq__post_obj?.anonymous}
|
||||
<div class="post__posted_by">
|
||||
Posted by: <span class="fas fa-user-secret"></span>
|
||||
<span class="post__full_name">Anonymous</span>
|
||||
@@ -251,7 +264,20 @@ $effect(() => {
|
||||
class="post_comment__posted_by comment_by_container">
|
||||
<span class="comment_by_lable"
|
||||
>Comment by:</span>
|
||||
{#if post_comment_obj.anonymous}
|
||||
{#if $ae_loc.trusted_access && post_comment_obj.anonymous}
|
||||
<span class="fas fa-user-secret"></span>
|
||||
<span
|
||||
class="comment_by_full_name anonymous"
|
||||
>
|
||||
{post_comment_obj.full_name}
|
||||
*Anonymous*
|
||||
</span>
|
||||
{#if $ae_loc.trusted_access && post_comment_obj?.email}
|
||||
<a
|
||||
href="mailto:{post_comment_obj?.email}?subject=IDAA BB Post Comment"
|
||||
>{post_comment_obj?.email}</a>
|
||||
{/if}
|
||||
{:else if post_comment_obj.anonymous}
|
||||
<span class="fas fa-user-secret"></span>
|
||||
<span
|
||||
class="comment_by_full_name anonymous"
|
||||
|
||||
@@ -160,7 +160,22 @@ onMount(() => {
|
||||
|
||||
<div
|
||||
class="ae_footer ae_meta post__meta m-1 flex flex-row items-center justify-center gap-2 text-sm text-gray-500/80">
|
||||
{#if idaa_post_obj.anonymous}
|
||||
{#if $ae_loc.trusted_access && idaa_post_obj.anonymous}
|
||||
<div class="post__posted_by">
|
||||
<span class="text-xs">Posted by:</span>
|
||||
<span class="fas fa-user-secret"></span>
|
||||
<span class="post__full_name">
|
||||
{idaa_post_obj.full_name}
|
||||
*Anonymous*
|
||||
</span>
|
||||
{#if idaa_post_obj.email}
|
||||
(<a
|
||||
href="mailto:{idaa_post_obj.email}?subject=IDAA BB Post"
|
||||
>{idaa_post_obj.email}</a
|
||||
>)
|
||||
{/if}
|
||||
</div>
|
||||
{:else if idaa_post_obj.anonymous}
|
||||
<div class="post__posted_by">
|
||||
<span class="text-xs">Posted by:</span>
|
||||
<span class="fas fa-user-secret"></span>
|
||||
|
||||
@@ -73,6 +73,14 @@ let no_results_no_filters = $derived(
|
||||
!($idaa_loc.recovery_meetings.qry__fulltext_str?.trim())
|
||||
);
|
||||
|
||||
// True when any filter dimension is active — drives the guided empty state.
|
||||
let has_active_filters = $derived(
|
||||
!!$idaa_loc.recovery_meetings.qry__physical ||
|
||||
!!$idaa_loc.recovery_meetings.qry__virtual ||
|
||||
!!$idaa_loc.recovery_meetings.qry__type ||
|
||||
!!($idaa_loc.recovery_meetings.qry__fulltext_str?.trim())
|
||||
);
|
||||
|
||||
let show_cache_reset_btn = $state(false);
|
||||
let cache_reset_timer: any = null;
|
||||
|
||||
@@ -111,6 +119,13 @@ async function handle_cache_reset() {
|
||||
}
|
||||
$idaa_sess.recovery_meetings.search_version++;
|
||||
}
|
||||
function clear_filters() {
|
||||
$idaa_loc.recovery_meetings.qry__physical = null;
|
||||
$idaa_loc.recovery_meetings.qry__virtual = null;
|
||||
$idaa_loc.recovery_meetings.qry__type = null;
|
||||
$idaa_loc.recovery_meetings.qry__fulltext_str = null;
|
||||
$idaa_sess.recovery_meetings.search_version++;
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Standardized Reactive Search Pattern (Aether UI V3)
|
||||
@@ -404,11 +419,34 @@ if (browser) {
|
||||
|
||||
<Comp__event_obj_qry />
|
||||
|
||||
<Element_data_store
|
||||
ds_code="recovery_meetings_info"
|
||||
ds_type="html"
|
||||
class_li="rounded-lg preset-outlined-surface-200-800 m-auto p-2 space-y-2 w-full max-w-xl"
|
||||
show_edit_btn={true} />
|
||||
<div class="w-full max-w-xl mx-auto space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$idaa_loc.recovery_meetings.ds_info_collapsed =
|
||||
!($idaa_loc.recovery_meetings.ds_info_collapsed ?? false);
|
||||
}}
|
||||
class="novi_btn btn btn-sm w-full flex items-center justify-between
|
||||
rounded-lg preset-outlined-surface-200-800 hover:preset-tonal-surface
|
||||
opacity-60 hover:opacity-100 transition-all px-3 py-1"
|
||||
title={$idaa_loc.recovery_meetings.ds_info_collapsed
|
||||
? 'Show meeting info'
|
||||
: 'Collapse meeting info'}>
|
||||
<span class="text-sm">
|
||||
<span class="fas fa-info-circle mr-1 text-xs"></span>Meeting Info
|
||||
</span>
|
||||
<span class="fas text-xs opacity-60
|
||||
{$idaa_loc.recovery_meetings.ds_info_collapsed ? 'fa-chevron-down' : 'fa-chevron-up'}">
|
||||
</span>
|
||||
</button>
|
||||
{#if !($idaa_loc.recovery_meetings.ds_info_collapsed ?? false)}
|
||||
<Element_data_store
|
||||
ds_code="recovery_meetings_info"
|
||||
ds_type="html"
|
||||
class_li="rounded-lg preset-outlined-surface-200-800 m-auto p-2 space-y-2 w-full"
|
||||
show_edit_btn={true} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if Array.isArray(event_id_li) && event_id_li.length}
|
||||
<Comp__event_obj_li_wrapper
|
||||
@@ -441,24 +479,40 @@ if (browser) {
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
||||
No recovery meetings found matching your criteria.
|
||||
</div>
|
||||
{#if show_cache_reset_btn}
|
||||
<!-- Escape hatch: surfaces after 8s when zero results + no active filters.
|
||||
With ~140 active meetings, zero unfiltered results always indicates
|
||||
stale IDB data. Clears the event cache and triggers a fresh API fetch. -->
|
||||
{#if has_active_filters}
|
||||
<!-- Guided empty state: filters are active but returned no results.
|
||||
Distinct from the zero-unfiltered-results path (which indicates a data problem).
|
||||
Here the member may simply have narrowed too far — offer a one-click escape. -->
|
||||
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p class="text-sm opacity-75">Still not seeing meetings? Your local cache may be out of date.</p>
|
||||
<p>No meetings found for these filters.</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-warning m-auto"
|
||||
onclick={handle_cache_reset}>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Refresh Meeting Cache
|
||||
class="btn btn-sm preset-tonal-primary m-auto"
|
||||
onclick={clear_filters}>
|
||||
<span class="fas fa-times m-1"></span>
|
||||
Clear all filters
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
||||
No recovery meetings found matching your criteria.
|
||||
</div>
|
||||
{#if show_cache_reset_btn}
|
||||
<!-- Escape hatch: surfaces after 8s when zero results + no active filters.
|
||||
With ~140 active meetings, zero unfiltered results always indicates
|
||||
stale IDB data. Clears the event cache and triggers a fresh API fetch. -->
|
||||
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p class="text-sm opacity-75">Still not seeing meetings? Your local cache may be out of date.</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-warning m-auto"
|
||||
onclick={handle_cache_reset}>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Refresh Meeting Cache
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -38,10 +38,25 @@ import {
|
||||
import Event_obj_id_edit from '../ae_idaa_comp__event_obj_id_edit.svelte';
|
||||
import Event_obj_id_view from '.././ae_idaa_comp__event_obj_id_view.svelte';
|
||||
import Help_tech from '$lib/app_components/e_app_help_tech.svelte';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
// *** Quickly pull out data from parent(s)
|
||||
let ae_acct = $derived(data[data.account_id]);
|
||||
|
||||
// Favorites stored in data_store (code: idaa_meetings_favorites, scoped to IDAA account_id).
|
||||
// Same shared record used by the meeting list view; see ae_idaa_comp__event_obj_li.svelte.
|
||||
let ds_fav_id = $state<string | null>(null);
|
||||
let ds_fav_json = $state<Record<string, string[]>>({});
|
||||
let fav_in_progress = $state(false);
|
||||
|
||||
let event_id_for_fav = $derived(ae_acct?.slct?.event_id ?? null);
|
||||
let is_fav = $derived.by(() => {
|
||||
const my_uuid = $idaa_loc.novi_uuid;
|
||||
if (!my_uuid || !event_id_for_fav) return false;
|
||||
const my_favs = ds_fav_json[my_uuid];
|
||||
return Array.isArray(my_favs) && my_favs.includes(event_id_for_fav);
|
||||
});
|
||||
|
||||
$idaa_sess.recovery_meetings.edit__event_obj = null;
|
||||
$effect(() => {
|
||||
if (!ae_acct) return;
|
||||
@@ -131,6 +146,55 @@ onMount(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (!$idaa_loc.novi_uuid || !$ae_api?.base_url) return;
|
||||
try {
|
||||
const result = await api.get_data_store({
|
||||
api_cfg: $ae_api,
|
||||
code: 'idaa_meetings_favorites'
|
||||
});
|
||||
const rec_id = result?.data_store_id || result?.id;
|
||||
if (rec_id) {
|
||||
ds_fav_id = rec_id;
|
||||
let raw = result.json ?? result.json_str ?? null;
|
||||
if (typeof raw === 'string') {
|
||||
try { raw = JSON.parse(raw); } catch { raw = {}; }
|
||||
}
|
||||
ds_fav_json = (raw as Record<string, string[]>) ?? {};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[favorites] Failed to load favorites data store:', err);
|
||||
}
|
||||
});
|
||||
|
||||
async function toggle_favorite() {
|
||||
const my_uuid = $idaa_loc.novi_uuid;
|
||||
const event_id = event_id_for_fav;
|
||||
if (!my_uuid || !event_id || !ds_fav_id || fav_in_progress) return;
|
||||
fav_in_progress = true;
|
||||
const current_user_list: string[] = Array.isArray(ds_fav_json[my_uuid])
|
||||
? [...ds_fav_json[my_uuid]] : [];
|
||||
const new_user_list = is_fav
|
||||
? current_user_list.filter((id) => id !== event_id)
|
||||
: [...current_user_list, event_id];
|
||||
const prev_json = ds_fav_json;
|
||||
const new_full_json = { ...ds_fav_json, [my_uuid]: new_user_list };
|
||||
ds_fav_json = new_full_json;
|
||||
try {
|
||||
await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: 'data_store',
|
||||
obj_id: ds_fav_id,
|
||||
fields: { json: JSON.stringify(new_full_json) }
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[favorites] API persist failed, rolling back:', err);
|
||||
ds_fav_json = prev_json;
|
||||
} finally {
|
||||
fav_in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
@@ -255,6 +319,23 @@ onDestroy(() => {
|
||||
<!-- <span class="fas fa-times m-1"></span> View Other Meetings -->
|
||||
</a>
|
||||
|
||||
{#if $idaa_loc.novi_uuid && ds_fav_id}
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggle_favorite}
|
||||
disabled={fav_in_progress}
|
||||
aria-pressed={is_fav}
|
||||
style="background:none; border:none; box-shadow:none; padding:4px 8px; cursor:pointer; line-height:1; opacity:{is_fav ? '1' : '0.5'}; transition:opacity 0.15s, color 0.15s;"
|
||||
title={is_fav ? 'Remove from My Meetings' : 'Add to My Meetings'}>
|
||||
{#if fav_in_progress}
|
||||
<span class="fas fa-spinner fa-spin" style="font-size:1rem;"></span>
|
||||
{:else}
|
||||
<span class="fas fa-star" style="font-size:1rem; color:{is_fav ? '#d97706' : 'currentColor'};"></span>
|
||||
{/if}
|
||||
<span style="font-size:0.85rem; margin-left:3px;">{is_fav ? 'In My Meetings' : 'Add to My Meetings'}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- View (default)/Edit event_id toggle -->
|
||||
{#if $idaa_sess.recovery_meetings.edit__event_obj}
|
||||
<button
|
||||
|
||||
@@ -19,6 +19,7 @@ let {
|
||||
}: Props = $props();
|
||||
|
||||
// *** Import Svelte specific
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
@@ -37,8 +38,86 @@ import {
|
||||
idaa_slct,
|
||||
idaa_trig
|
||||
} from '$lib/stores/ae_idaa_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
||||
|
||||
// Favorites are stored in a dedicated data_store record (code: idaa_meetings_favorites,
|
||||
// scoped to the IDAA account_id) so toggling never touches ae_event rows or their
|
||||
// updated_on timestamps. Structure: { [novi_uuid]: [event_id, ...] }.
|
||||
// One shared record per account — known race condition if two members toggle at
|
||||
// the exact same moment (last write wins). Acceptable for ~1000 members.
|
||||
let ds_fav_id = $state<string | null>(null);
|
||||
let ds_fav_json = $state<Record<string, string[]>>({});
|
||||
|
||||
// Tracks event IDs currently being toggled to prevent double-clicks
|
||||
let favorites_in_progress = $state<Set<string>>(new Set());
|
||||
|
||||
onMount(async () => {
|
||||
if (!$idaa_loc.novi_uuid || !$ae_api?.base_url) return;
|
||||
try {
|
||||
const result = await api.get_data_store({
|
||||
api_cfg: $ae_api,
|
||||
code: 'idaa_meetings_favorites'
|
||||
});
|
||||
const rec_id = result?.data_store_id || result?.id;
|
||||
if (rec_id) {
|
||||
ds_fav_id = rec_id;
|
||||
let raw = result.json ?? result.json_str ?? null;
|
||||
if (typeof raw === 'string') {
|
||||
try { raw = JSON.parse(raw); } catch { raw = {}; }
|
||||
}
|
||||
ds_fav_json = (raw as Record<string, string[]>) ?? {};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[favorites] Failed to load favorites data store:', err);
|
||||
}
|
||||
});
|
||||
|
||||
function check_fav(event_obj: Record<string, unknown>): boolean {
|
||||
const my_uuid = $idaa_loc.novi_uuid;
|
||||
if (!my_uuid) return false;
|
||||
const my_favs = ds_fav_json[my_uuid];
|
||||
return Array.isArray(my_favs) && my_favs.includes(event_obj.event_id as string);
|
||||
}
|
||||
|
||||
async function toggle_favorite(event_obj: Record<string, unknown>) {
|
||||
const my_uuid = $idaa_loc.novi_uuid;
|
||||
const event_id = event_obj.event_id as string;
|
||||
|
||||
if (!my_uuid || !event_id || !ds_fav_id || favorites_in_progress.has(event_id)) return;
|
||||
|
||||
favorites_in_progress.add(event_id);
|
||||
|
||||
const current_user_list: string[] = Array.isArray(ds_fav_json[my_uuid])
|
||||
? [...ds_fav_json[my_uuid]]
|
||||
: [];
|
||||
const already_fav = current_user_list.includes(event_id);
|
||||
const new_user_list = already_fav
|
||||
? current_user_list.filter((id) => id !== event_id)
|
||||
: [...current_user_list, event_id];
|
||||
|
||||
const prev_json = ds_fav_json;
|
||||
const new_full_json = { ...ds_fav_json, [my_uuid]: new_user_list };
|
||||
|
||||
// Optimistic update — ds_fav_json is reactive $state, so check_fav() re-evaluates
|
||||
// instantly everywhere it's called (derived list sort + each block template).
|
||||
ds_fav_json = new_full_json;
|
||||
|
||||
try {
|
||||
await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: 'data_store',
|
||||
obj_id: ds_fav_id,
|
||||
fields: { json: JSON.stringify(new_full_json) }
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[favorites] API persist failed, rolling back:', err);
|
||||
ds_fav_json = prev_json;
|
||||
} finally {
|
||||
favorites_in_progress.delete(event_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Derived list of visible items (Refactored 2026-02-05)
|
||||
//
|
||||
// WHY: The parent search logic fetches matching records into the local IndexedDB.
|
||||
@@ -78,8 +157,27 @@ let visible_event_obj_li = $derived.by(() => {
|
||||
`visible_event_obj_li: Input=${list.length}, Output=${filtered.length} (trusted=${$ae_loc.trusted_access})`
|
||||
);
|
||||
|
||||
// Favorites: pin starred meetings to the top of the list.
|
||||
// Runs before the limit slice so favorites are never cut off.
|
||||
const my_uuid = $idaa_loc.novi_uuid;
|
||||
if (my_uuid) {
|
||||
filtered.sort((a, b) => {
|
||||
const a_fav = check_fav(a);
|
||||
const b_fav = check_fav(b);
|
||||
if (a_fav && !b_fav) return -1;
|
||||
if (!a_fav && b_fav) return 1;
|
||||
return 0; // stable — preserves existing sort within each group
|
||||
});
|
||||
}
|
||||
|
||||
// Favorites-only filter — applied after sort so starred items are already at top
|
||||
const starred_only = $idaa_loc.recovery_meetings.qry__favorites_only;
|
||||
const shown = starred_only && my_uuid
|
||||
? filtered.filter((item) => check_fav(item))
|
||||
: filtered;
|
||||
|
||||
// Final safety slice to respect the user's limit selection
|
||||
return filtered.slice(0, $idaa_loc.recovery_meetings.qry__limit || 150);
|
||||
return shown.slice(0, $idaa_loc.recovery_meetings.qry__limit || 150);
|
||||
});
|
||||
$effect(() => {
|
||||
if (log_lvl) {
|
||||
@@ -129,10 +227,33 @@ $effect(() => {
|
||||
flex-row items-center justify-between gap-2 text-2xl
|
||||
font-bold
|
||||
">
|
||||
<span
|
||||
class="fas fa-calendar-day text-neutral-800/80"
|
||||
></span>
|
||||
{idaa_event_obj?.name ?? 'Recovery Meeting'}
|
||||
<span class="flex flex-row items-center gap-2">
|
||||
<span
|
||||
class="fas fa-calendar-day text-neutral-800/80"
|
||||
></span>
|
||||
{idaa_event_obj?.name ?? 'Recovery Meeting'}
|
||||
</span>
|
||||
|
||||
{#if $idaa_loc.novi_uuid}
|
||||
{@const fav = check_fav(idaa_event_obj)}
|
||||
{@const pending = favorites_in_progress.has(idaa_event_obj?.event_id)}
|
||||
<!-- Inline style resets Bootstrap v3 .btn defaults (the iframe
|
||||
injects idaa.css which applies its own button box model).
|
||||
No Skeleton preset classes here — they fight with Bootstrap. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => toggle_favorite(idaa_event_obj)}
|
||||
disabled={pending}
|
||||
aria-pressed={fav}
|
||||
style="background:none; border:none; box-shadow:none; padding:2px 6px; cursor:pointer; line-height:1; opacity:{fav ? '1' : '0.35'}; transition:opacity 0.15s, color 0.15s;"
|
||||
title={fav ? 'Remove from My Meetings' : 'Add to My Meetings'}>
|
||||
{#if pending}
|
||||
<span class="fas fa-spinner fa-spin" style="font-size:1.1rem;"></span>
|
||||
{:else}
|
||||
<span class="fas fa-star" style="font-size:1.1rem; color:{fav ? '#d97706' : 'currentColor'};"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
<div
|
||||
@@ -557,6 +678,13 @@ $effect(() => {
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
Loading...
|
||||
</div>
|
||||
{:else if $idaa_loc.recovery_meetings.qry__favorites_only}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<span class="fas fa-star text-2xl text-yellow-400"></span>
|
||||
<p>No starred meetings yet.</p>
|
||||
<p class="text-sm opacity-75">Tap the ★ on any meeting card to add it to My Meetings.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
||||
|
||||
@@ -37,6 +37,14 @@ import Help_tech from '$lib/app_components/e_app_help_tech.svelte';
|
||||
|
||||
let ae_promises: key_val = $state({});
|
||||
|
||||
// Predefined limit steps for the +/- stepper. Trusted staff get larger options
|
||||
// since they often query with disabled/hidden meetings included.
|
||||
let limit_steps = $derived($ae_loc.trusted_access ? [25, 50, 75, 100, 150, 200, 500] : [25, 50, 75, 100, 150]);
|
||||
let limit_idx = $derived.by(() => {
|
||||
const idx = limit_steps.indexOf($idaa_loc.recovery_meetings.qry__limit);
|
||||
return idx >= 0 ? idx : limit_steps.length - 1;
|
||||
});
|
||||
|
||||
// *** Functions and Logic
|
||||
/**
|
||||
* Reactive Search Trigger
|
||||
@@ -56,6 +64,25 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
fn(event);
|
||||
};
|
||||
}
|
||||
|
||||
const sort_modes = [
|
||||
{ mode: 'updated_on', label: 'Last Updated', icon: 'fa-clock', order_by_li: { priority: 'DESC', sort: 'DESC', updated_on: 'DESC', created_on: 'DESC', name: 'ASC' } },
|
||||
{ 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' } },
|
||||
{ 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' } }
|
||||
];
|
||||
|
||||
let sort_cycle_idx = $derived.by(() => {
|
||||
const idx = sort_modes.findIndex(m => m.mode === $idaa_loc.recovery_meetings.qry__order_by);
|
||||
return idx >= 0 ? idx : 0;
|
||||
});
|
||||
let current_sort = $derived(sort_modes[sort_cycle_idx]);
|
||||
|
||||
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();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- preset-filled-surface-200-800 -->
|
||||
@@ -160,369 +187,291 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter chips: My Meetings + 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-2xl flex-row flex-wrap items-center justify-center 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>
|
||||
<!-- 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}
|
||||
|
||||
<!-- <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>
|
||||
<!-- 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="sr-only">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>
|
||||
|
||||
<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="sr-only">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] (val)}
|
||||
{@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>
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="ae_group ae_row flex w-full max-w-full flex-row flex-wrap items-center justify-center gap-2 p-1">
|
||||
<!-- Max events select options -->
|
||||
<span class="flex flex-row items-center justify-around gap-1">
|
||||
<label
|
||||
class="form-group w-32 text-sm md:w-42"
|
||||
for="qry_limit__events">
|
||||
Max results:
|
||||
<div
|
||||
class="ae_group ae_row flex w-full flex-row flex-wrap items-center justify-center gap-2 p-1">
|
||||
<!-- Sort: single cycling button -->
|
||||
<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>
|
||||
|
||||
<select
|
||||
id="qry_limit__events"
|
||||
bind:value={$idaa_loc.recovery_meetings.qry__limit}
|
||||
class="
|
||||
select preset-tonal-tertiary preset-outline-tertiary-200-800 form-control
|
||||
col-sm-12
|
||||
inline-block
|
||||
w-20
|
||||
px-1 text-sm
|
||||
">
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={75}>75</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={150}>150</option>
|
||||
<option value={200} class:hidden={!$ae_loc.trusted_access}
|
||||
>200</option>
|
||||
<option value={500} class:hidden={!$ae_loc.trusted_access}
|
||||
>500</option>
|
||||
</select>
|
||||
</label>
|
||||
<!-- </span> -->
|
||||
<!-- Max results: +/- stepper through predefined steps -->
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<span class="sr-only">Max results:</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { if (limit_idx > 0) $idaa_loc.recovery_meetings.qry__limit = limit_steps[limit_idx - 1]; }}
|
||||
disabled={limit_idx <= 0}
|
||||
title="Show fewer results"
|
||||
class="novi_btn btn btn-sm preset-outlined-surface-300-700 transition-all
|
||||
{limit_idx <= 0 ? 'opacity-30 cursor-not-allowed' : 'opacity-60 hover:opacity-100'}">
|
||||
<span class="fas fa-minus"></span>
|
||||
</button>
|
||||
<span
|
||||
class="min-w-10 text-center text-sm font-semibold tabular-nums"
|
||||
title="Max results shown">
|
||||
{$idaa_loc.recovery_meetings.qry__limit}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { if (limit_idx < limit_steps.length - 1) $idaa_loc.recovery_meetings.qry__limit = limit_steps[limit_idx + 1]; }}
|
||||
disabled={limit_idx >= limit_steps.length - 1}
|
||||
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>
|
||||
|
||||
<!-- Sort by last updated date or by name -->
|
||||
<!-- <span
|
||||
class="flex flex-row gap-1 items-center justify-around"
|
||||
> -->
|
||||
<label
|
||||
class="form-group w-32 text-sm md:w-42"
|
||||
for="qry_order_by__events">
|
||||
Sort by:
|
||||
<!-- Staff: toggle hidden events visibility (trusted + 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')}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$idaa_loc.recovery_meetings.qry__hidden = 'all';
|
||||
$idaa_loc.recovery_meetings.qry__limit = 200;
|
||||
}}
|
||||
class="novi_btn btn_show_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 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}
|
||||
|
||||
<select
|
||||
id="qry_order_by__events"
|
||||
bind:value={$idaa_loc.recovery_meetings.qry__order_by}
|
||||
onchange={() => {
|
||||
const mode = $idaa_loc.recovery_meetings.qry__order_by;
|
||||
if (mode === 'updated_on') {
|
||||
$idaa_loc.recovery_meetings.qry__order_by_li = {
|
||||
priority: 'DESC',
|
||||
sort: 'DESC',
|
||||
updated_on: 'DESC',
|
||||
created_on: 'DESC',
|
||||
name: 'ASC'
|
||||
};
|
||||
} else if (mode === 'name_asc') {
|
||||
$idaa_loc.recovery_meetings.qry__order_by_li = {
|
||||
priority: 'DESC',
|
||||
sort: 'DESC',
|
||||
name: 'ASC',
|
||||
updated_on: 'DESC',
|
||||
created_on: 'DESC'
|
||||
};
|
||||
} else if (mode === 'name_desc') {
|
||||
$idaa_loc.recovery_meetings.qry__order_by_li = {
|
||||
priority: 'DESC',
|
||||
sort: 'DESC',
|
||||
name: 'DESC',
|
||||
updated_on: 'DESC',
|
||||
created_on: 'DESC'
|
||||
};
|
||||
}
|
||||
handle_search_trigger();
|
||||
}}
|
||||
class="
|
||||
select preset-tonal-tertiary preset-outline-tertiary-200-800 form-control
|
||||
col-sm-12
|
||||
inline-block
|
||||
w-40
|
||||
px-1 text-sm
|
||||
">
|
||||
<option value="updated_on">Last Updated</option>
|
||||
<option value="name_asc">Meeting Name (A-Z)</option>
|
||||
<option value="name_desc">Meeting Name (Z-A)</option>
|
||||
</select>
|
||||
</label>
|
||||
</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">
|
||||
{#if $ae_loc.edit_mode && $ae_loc.trusted_access && (!$idaa_loc.recovery_meetings.qry__hidden || $idaa_loc.recovery_meetings.qry__hidden == 'not_hidden')}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$idaa_loc.recovery_meetings.qry__hidden = 'all';
|
||||
$idaa_loc.recovery_meetings.qry__limit = 200;
|
||||
}}
|
||||
class="
|
||||
novi_btn btn_show_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 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}
|
||||
<!-- Create new meeting (Novi members and trusted staff in edit mode) -->
|
||||
{#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 = {};
|
||||
|
||||
{#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>
|
||||
|
||||
<span class="flex flex-row items-center justify-around gap-1">
|
||||
{#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
|
||||
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 {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
let data_kv = {
|
||||
name: 'Change NEW Recovery Meeting Name',
|
||||
external_person_id: novi_uuid
|
||||
};
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
'Creating new event (recovery meeting) with data_kv:',
|
||||
data_kv
|
||||
);
|
||||
}
|
||||
let data_kv = {
|
||||
name: 'Change NEW Recovery Meeting Name',
|
||||
external_person_id: novi_uuid
|
||||
};
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
'Creating new event (recovery meeting) with data_kv:',
|
||||
data_kv
|
||||
);
|
||||
}
|
||||
|
||||
events_func
|
||||
.create_ae_obj__event({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
data_kv: data_kv,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((results) => {
|
||||
if (results) {
|
||||
$idaa_slct.event_id = results.event_id;
|
||||
$idaa_slct.event_obj = { ...results };
|
||||
$idaa_sess.recovery_meetings.edit__event_obj =
|
||||
results.event_id;
|
||||
events_func
|
||||
.create_ae_obj__event({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
data_kv: data_kv,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((results) => {
|
||||
if (results) {
|
||||
$idaa_slct.event_id = results.event_id;
|
||||
$idaa_slct.event_obj = { ...results };
|
||||
$idaa_sess.recovery_meetings.edit__event_obj =
|
||||
results.event_id;
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`New event created with ID: ${results.event_id}`
|
||||
);
|
||||
}
|
||||
goto(
|
||||
`/idaa/recovery_meetings/${results.event_id}`
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`New event created with ID: ${results.event_id}`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error creating event:', error);
|
||||
alert('Failed to create new event.');
|
||||
});
|
||||
}}
|
||||
class="
|
||||
novi_btn btn-tertiary
|
||||
btn
|
||||
btn-sm preset-tonal-warning
|
||||
preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs
|
||||
transition
|
||||
"
|
||||
disabled={!$ae_loc.authenticated_access}>
|
||||
<span class="fas fa-plus m-1"></span> Create New Meeting
|
||||
</button>
|
||||
{/if}
|
||||
goto(
|
||||
`/idaa/recovery_meetings/${results.event_id}`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error creating event:', error);
|
||||
alert('Failed to create new event.');
|
||||
});
|
||||
}}
|
||||
class="novi_btn btn-tertiary btn btn-sm preset-tonal-warning
|
||||
preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs transition"
|
||||
disabled={!$ae_loc.authenticated_access}>
|
||||
<span class="fas fa-plus m-1"></span> Create New Meeting
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if $ae_loc.edit_mode && $ae_loc.trusted_access}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if (!confirm('Download exported data Excel file?')) {
|
||||
return false;
|
||||
}
|
||||
ae_promises.download__events_export =
|
||||
core_func.download_export__obj_type({
|
||||
api_cfg: $ae_api,
|
||||
get_obj_type: 'event',
|
||||
for_obj_type: 'account',
|
||||
for_obj_id: $ae_loc.account_id,
|
||||
exp_alt: 'idaa',
|
||||
file_type: 'Excel',
|
||||
return_file: true,
|
||||
filename: `${$ae_loc.account_code}_IDAA_Recovery_Meetings_export_${ae_util.iso_datetime_formatter()}.xlsx`,
|
||||
auto_download: true,
|
||||
log_lvl: 1
|
||||
});
|
||||
}}
|
||||
class="
|
||||
novi_btn btn-tertiary
|
||||
btn
|
||||
btn-sm preset-tonal-warning
|
||||
preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs
|
||||
transition
|
||||
"
|
||||
title={`Download meeting data for ${$ae_loc.account_name}`}>
|
||||
{#await ae_promises.download__events_export}
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
{:then}
|
||||
<span class="fas fa-download m-1"></span>
|
||||
{/await}
|
||||
Export All Data
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Staff: export all meeting data to Excel (trusted + edit mode) -->
|
||||
{#if $ae_loc.edit_mode && $ae_loc.trusted_access}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if (!confirm('Download exported data Excel file?')) {
|
||||
return false;
|
||||
}
|
||||
ae_promises.download__events_export =
|
||||
core_func.download_export__obj_type({
|
||||
api_cfg: $ae_api,
|
||||
get_obj_type: 'event',
|
||||
for_obj_type: 'account',
|
||||
for_obj_id: $ae_loc.account_id,
|
||||
exp_alt: 'idaa',
|
||||
file_type: 'Excel',
|
||||
return_file: true,
|
||||
filename: `${$ae_loc.account_code}_IDAA_Recovery_Meetings_export_${ae_util.iso_datetime_formatter()}.xlsx`,
|
||||
auto_download: true,
|
||||
log_lvl: 1
|
||||
});
|
||||
}}
|
||||
class="novi_btn btn-tertiary btn btn-sm preset-tonal-warning
|
||||
preset-outlined-warning-200-800 hover:preset-filled-warning-200-800 text-xs transition"
|
||||
title={`Download meeting data for ${$ae_loc.account_name}`}>
|
||||
{#await ae_promises.download__events_export}
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
{:then}
|
||||
<span class="fas fa-download m-1"></span>
|
||||
{/await}
|
||||
Export All Data
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user