Skip to content

feat(app): public witnessing - #5297

Open
ux-git wants to merge 28 commits into
sws2apps:mainfrom
ux-git:feat_public_witnessing
Open

feat(app): public witnessing#5297
ux-git wants to merge 28 commits into
sws2apps:mainfrom
ux-git:feat_public_witnessing

Conversation

@ux-git

@ux-git ux-git commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

Public witnessing: locations management plus day, week and month views where publishers book shifts. Frontend and local-first data layer — only the congregation sync endpoint is left for a later PR.

Follows the Figma sections "Public witnessing MVP", "Shift booking flow", and the Monthly/Daily view frames.

Data model

Two Dexie tables (public_witnessing_locations, public_witnessing_arrangements) on schema v13, following the usual vertical slice: _deleted/updatedAt data blobs, soft-delete services flagging metadata.send_local, jotai atoms through the IndexedDb wrapper.

Two decisions worth a look:

  • Shifts are virtual. A day's slots are derived at render time from the location's per-weekday schedule merged with its arrangements — nothing is stored per date, so editing a location's hours never migrates rows.
  • One record per booking action (author in created_by), so two publishers booking the same slot offline merge cleanly. Joining a partner-seeker writes the joiner's own record and never mutates the seeker's; the orange "partner needed" state is derived.

Views

Day, week and month share one derivation (useShiftsCard) and one cell component, switched by the TabSwitcher from #5291.

  • Day — full-width rows with a status badge.
  • Week — seven columns, one grid row per shift so the cells stay aligned across days; a booked cell keeps one line and reveals its publishers behind an expand chevron.
  • Month — the same calendar as the field service meetings PR (feat(app): field service meetings #4369). Its grid was extracted into @components/month_calendar (weekday header, continuous cell borders, outline stepping away after the last day, today dot), with the day content left to the caller: here a dashed "N occupied" over a filled "N available" that turns red when nothing is left, and a single outlined count on phones.

Week and month respect the congregation's first day of the week. Selecting a month day opens it in the day view.

Locations and scheduling

Admin only (app admin + service overseer). Sidebar reuses settings_tab from #5224; add/edit dialog has Details and Schedule tabs, soft delete, and drag reorder persisting sort_index.

The schedule tab offers an Every day switch — on, one set of shifts repeats all week; off, each approved day gets its own. The checkbox approves a day, the rest of the row opens it for editing.

Booking

The "Confirm the arrangement" dialog covers the Figma flows: partner needed with a wanted count, "I have a partner" with a persons autocomplete that accepts free text, the admin For myself / For others tabs, joining a seeker, editing/cancelling, and the add-to-calendar ICS prompt.

Capacity is enforced against the shift, not just the location: seats left = max_publishers minus what other arrangements already hold, minus one for the author when arranging for oneself. That caps the name fields, the wanted count, and what is saved. A shift with one seat left offers no partner options at all.

Notes for reviewers

  • @components/badge changed for the whole app. Every size passed its font size through sx, where the default body-small-semibold class overruled it — small badges rendered at 14px instead of the 12px they are drawn at, big ones at 14px instead of their own step. Each size now picks its class from the type scale (an explicit className still wins), plus a new light fill and small radius 2px → 4px. Worth eyeballing badges elsewhere.
  • @components/month_calendar is new and shared; feat(app): field service meetings #4369 can move onto it when it lands.
  • Navbar titles centre on mobile (left from tablet688 up), using the balancing-spacer approach from feat(reports): counter for meeting attendance records #5291.
  • IconReorder fix: its centre dot was a separate <circle> the icon recolouring never reached, so it kept a hardcoded fill. Merged into the arrows path — affects every existing usage.
  • Locale keys are reused where the wording already existed (tr_PW*, tr_occupied/tr_available, tr_startTime/tr_endTime); only genuinely new strings were added.
  • Entry point is the Ministry dashboard card. Not included: the location map tab, backend sync.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
staging-organized-app Ready Ready Preview Jul 31, 2026 8:14am
test-organized-app Ready Ready Preview Jul 31, 2026 8:14am

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ux-git, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb16e005-f0ca-4e6e-b236-ed917412afde

📥 Commits

Reviewing files that changed from the base of the PR and between 639a15a and bc09828.

⛔ Files ignored due to path filters (2)
  • src/locales/en/general.json is excluded by !**/*.json
  • src/locales/en/ministry.json is excluded by !**/*.json
📒 Files selected for processing (24)
  • src/components/badge/index.types.ts
  • src/components/index.ts
  • src/components/public_witnessing_card/index.tsx
  • src/components/public_witnessing_card/public_witnessing_card.styles.ts
  • src/components/public_witnessing_card/public_witnessing_card.types.ts
  • src/components/public_witnessing_card/public_witnessing_place_card.tsx
  • src/components/public_witnessing_card/public_witnessing_time_card.tsx
  • src/components/public_witnessing_card/view/day.tsx
  • src/components/public_witnessing_card/view/default.tsx
  • src/components/select/index.styles.tsx
  • src/components/settings_tab/index.tsx
  • src/components/stepper/index.tsx
  • src/components/stepper/index.types.ts
  • src/components/textfield/index.tsx
  • src/definition/public_witnessing.ts
  • src/features/app_start/vip/congregation_create/index.tsx
  • src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx
  • src/features/ministry/public_witnessing/location_details/index.tsx
  • src/features/ministry/public_witnessing/location_form/index.tsx
  • src/features/ministry/public_witnessing/location_form/useLocationForm.tsx
  • src/features/ministry/public_witnessing/shifts_card/shift_cell.tsx
  • src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx
  • src/features/ministry/public_witnessing/shifts_card/week_view.tsx
  • src/services/dexie/public_witnessing_arrangements.ts

Walkthrough

Adds a publisher-protected Public Witnessing feature with location management, recurring shift schedules, calendar views, publisher arrangements, IndexedDB persistence, responsive layouts, and reusable navigation components.

Changes

Public witnessing

Layer / File(s) Summary
Contracts and shared UI
src/definition/public_witnessing.ts, src/components/*
Adds public witnessing types and reusable badge, month-calendar, settings-tab, and tab-switcher components.
IndexedDB and state integration
src/indexedDb/*, src/services/dexie/*, src/states/public_witnessing.ts, src/wrapper/database_indexeddb/*
Adds location and arrangement stores, metadata, persistence helpers, Jotai atoms, and IndexedDB loading.
Location management
src/features/ministry/public_witnessing/location_*/*, src/features/ministry/public_witnessing/locations_*/*, src/features/ministry/public_witnessing/usePermissions.tsx
Adds location listing, details, forms, schedules, deletion, reordering, and management permissions.
Shift and arrangement workflow
src/features/ministry/public_witnessing/shifts_card/*, src/features/ministry/public_witnessing/arrangement_form/*, src/utils/date.ts
Adds day, week, and month shift views with status calculation and arrangement creation, editing, deletion, partner handling, and calendar downloads.
Page routing and responsive presentation
src/App.tsx, src/pages/dashboard/ministry/index.tsx, src/pages/ministry/public_witnessing/*, src/features/ministry/public_witnessing/index.tsx, src/layouts/navbar/index.tsx
Adds the dashboard entry, protected routes, page orchestration, responsive location navigation, and navbar title truncation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Publisher
  participant PublicWitnessing
  participant ShiftsCard
  participant ArrangementForm
  participant IndexedDB
  Publisher->>PublicWitnessing: open protected route
  PublicWitnessing->>IndexedDB: load locations and arrangements
  PublicWitnessing->>ShiftsCard: display selected location
  Publisher->>ShiftsCard: select shift slot
  ShiftsCard->>ArrangementForm: open arrangement form
  ArrangementForm->>IndexedDB: save or delete arrangement
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: adding the Public Witnessing feature.
Description check ✅ Passed The description directly explains the Public Witnessing UI, booking flows, data layer, scope, and implementation details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ux-git and others added 19 commits July 30, 2026 18:29
Locations and arrangements tables follow the established vertical slice:
definition types with the _deleted/updatedAt data-blob pattern, Dexie
v13 schema, soft-delete services flagging metadata send_local, jotai db
atoms wired through the IndexedDb wrapper, and a publisher route with a
Ministry dashboard entry (reusing the existing tr_PW locale key).

Arrangements are one record per booking action (author in created_by)
so concurrent offline bookings merge without conflicts; day-view shifts
are derived from the location's per-weekday schedule and never stored.
Two-column page: locations sidebar (settings_tab from the settings
redesign, PR sws2apps#5224) and a details card with badges and description.
Admin actions gated by usePermissions (app admin + service overseer):
add/edit dialog with General information and Schedule tabs (approved
days + per-day shift ranges), delete confirmation (soft delete), and
drag reorder persisting sort_index.

Shift time rows memoize their Date values in a dedicated ShiftRow —
the shared TimePicker resets its in-progress edit whenever the value
prop identity changes. Locale keys reuse the existing tr_PW*/general
strings; only genuinely new labels were added.
Shifts card composes day navigation (arrows + jump-to-today, matching
the field-service-meetings calendar pattern) with the segmented
TabSwitcher from the clicker PR (week/month disabled for now). Day
slots are derived at render time from the location's weekday schedule
merged with booked arrangements — nothing is materialized per date.

Cell states follow the design spec: available (accent border + chip),
partner needed (orange, stays open until the requested partners join),
fully occupied (dashed) and past. Cells are interactive only for
bookable states, the arrangement author, or admins.

The confirm-the-arrangement dialog covers all flows: partner needed
with a wanted-count, I-have-a-partner with a freeSolo persons
autocomplete (non-app partners stay name-only), admin for-myself /
for-others tabs, joining a seeker (creates the joiner's own record —
derived state keeps records conflict-free), edit and cancel, and an
add-to-calendar ICS prompt after booking.
Schedule tab now matches the design measurements: fixed 240px approved
days column, vertical divider with 32px gaps, 16px shifts-column
spacing, 48px day rows, and a 16px dash between the start/end time
fields. Tab content renders through the Tabs component's own panels,
removing the double gap the empty tabpanels used to add (same fix in
the arrangement dialog).

Header actions reordered to Reorder / Edit / Add with Add as the main
button, and the max publishers badge shortened to "Max. publishers".
Shift cells now behave like buttons: outlined states fill with
accent-100 on hover instead of casting a shadow, and publisher names
use the smaller text style. The day header drops the year from its
label and paints the jump-to-today icon black; the no-shifts state
uses the standard info-icon message row.

IconReorder's center dot was a separate circle element that CSS
recoloring (svg g path selectors) never reached — merged into the
arrows path so the glyph is one shape. The cart badge icon gets an
explicit accent-dark color for the same reason: its paths sit directly
under the svg root with a hardcoded default fill.

Location dialog: badges use the small size, the storage field label is
now "Stored at", "Every day" shows an indeterminate state when only
some days are approved, Add shift uses the small button variant, shift
delete buttons sit in the 48px rounded container used by the circuit
overseer week rows, and an unselected schedule shows a select-a-day
hint with no divider.
The shift-row trash button cancels the IconButton edge-start negative
margin that was eating its left spacing, and the day checkboxes drop
the FormControlLabel offset so the row's 8px inner padding matches the
design. The location delete action moves from a full-width button at
the dialog bottom to a small red button beside the Edit location title.

Max. publishers per shift is now optional: the field may stay empty,
the badge hides when unset, and shift capacity falls back to the
default of 3.
Mobile now mirrors the app's subpage pattern: the page shows only the
locations list, and tapping a location opens its details as a subpage
using the SubpageNavbar from the clicker PR (location name with the
parent page underneath, animated back arrow returning to the list).

The day navigation adopts the field service meetings calendar layout:
single row on tablet+ with the toggle right-aligned, stacked on mobile
with the arrows pushed to the card edges; the jump-to-today control
always reserves its slot so the label never shifts, the label gets a
180px centered slot with ellipsis, and min-width guards keep the
switcher and header clipped inside the card.
On mobile the schedule tab no longer stacks a detached shifts panel
under the day list — the editor expands inline right under the tapped
day, with expand/collapse chevrons on the far right of approved days
and tap-to-toggle behavior. Desktop keeps the two-column layout.

Small buttons (Add shift, Add person, the title Delete) now pass
disableAutoStretch so they stay compact instead of stretching to the
global full-width mobile button default.
The mobile detail view was rendering its own SubpageNavbar below the
app navbar, so the screen showed two headers. The app navbar already
renders a back arrow with a title and secondary title, so the detail
is now a real route (/public-witnessing/:locationId) that drives it
through PageTitle: one header, and the navbar back arrow returns to
the list on its own. The cherry-picked SubpageNavbar is dropped — it
exists for fullscreen overlays that cover the navbar, not for this.

Also: the empty locations state uses the InfoNote card variant, the
locations list keeps its chevron visible on touch devices (hover never
fires there), the dashboard entry uses the cart icon, and a day's
shifts only open once the day is approved — tapping an unapproved row
approves it, unchecking closes its shifts again.
The shifts card now switches between the day, week and month periods
instead of only showing a single day:

- week: seven aligned columns of compact shift cells on wide screens,
  a stacked per-day list below it
- month: a calendar grid with the available/occupied shift counts per
  day (dots when the card is narrow); tapping a day opens its day view
- navigation, the period label and the jump-to-today control follow the
  selected period

Slots now carry their own date, so booking from the week and month
views stores the clicked day instead of the anchor date, and the
arrangement dialog shows it. Arrangements are indexed once per render
instead of being scanned for every shift, and the shift cells are
reachable by keyboard.

Also narrows the locations sidebar below 1400px and lets the card
header wrap, which fixes the horizontal overflow of the whole page
between 768px and 1200px.
Opening a location on mobile pushes its uid into the route, and the
container re-applied that uid on every render — so a desktop window
that landed on the detail route (a shared link, or a resized phone
session) could no longer switch locations from the sidebar. Wide
screens now fold the detail route back into the base one.
Review fixes across the day, week and month views:

- the status pill is now the shared Badge component (accent / orange /
  grey) and every non-past state is labelled, including "Fully
  occupied"; the hand-rolled chip is gone
- hover styles only apply to cells the user can actually open, so past
  and other-people's shifts no longer light up under the pointer, and
  partner-needed cells finally react
- week cells carry a tooltip and an aria-label with their state, which
  the compact layout previously left to colour alone; month cells name
  their counts in the label
- month days from the neighbouring months only mute their number
  instead of filling grey, matching the app's date picker, and the
  weekday row lines up with the date numbers
- the feature uses the shared Divider component instead of the raw MUI
  one, like the rest of the app
- getWeekStartDate moved to @utils/date (first-day-of-week aware and,
  unlike getWeekDate, non-mutating)
Month view now uses the same calendar as the field service meetings
one (PR sws2apps#4369): its grid was extracted into @components/month_calendar
— weekday header row, continuous cell borders, leading blanks filled
and the outline stepping away after the last day, today's dot — with
the day content left to the caller. Public witnessing fills it with
the designed badges: a dashed "N occupied" over a filled "N available"
that turns red when nothing is left. Days already over only report
what was served instead of shouting "0 available".

Week view follows its design too: each column is headed by the date
and the weekday (muted once the day is over), and a booked cell keeps
one line, revealing its publishers (and the partner-needed note)
behind an expand chevron. Free cells keep the Available badge.

Figma: section 5194:177629 — "Monthly view" 5265:217337, booking-flow
"Publisher view" 5381:227347.
Schedule tab: the "every day" checkbox that sat above the seven weekday
rows is replaced by an every-day / selected-days switcher — every day
edits one set of shifts for the whole week, selected days keeps the per
day editor. The checkbox alone approves a day now; the rest of the row
opens it, with a chevron appearing under the pointer to say so, and the
"select a day" hint moved to the top of the editor pane.

Calendar: the weekday header uses the same border scheme as the cells,
so its column lines meet theirs, and the rounded corners belong to the
corner cells instead of a clipping wrapper. Occupied/available badges
drop to the small label size; phone-sized cells show a single outlined
count, coloured by what is still open, pinned to the bottom of the day.

Week view: 16px column gaps with a divider down the middle, no
Available badge (the tooltip covers it, after two seconds), smaller
times and room between them and the expand chevron.

Mobile: the navbar title centres between the back arrow and its
balancing spacer (as in the clicker mode), the locations list drops the
active indicator's slot down to an 8px inset, editing is offered inside
a location and reordering on the list, and moving between the two
slides in from the matching edge.
…holds

The partner limit was derived from the location's maximum alone, with a
floor of one, and it ignored the publishers already arranged for the
shift — so a two-publisher location offered a name field per remaining
step and could be saved with more people than the shift allows.

The form now counts the seats other arrangements already hold, keeps one
for the author when arranging for oneself, and caps the name fields, the
"partner needed" count and what gets saved to what is actually left.
A shift with a single seat left simply takes its author, without
offering partners at all.

Also shortens the location dialog's first tab to "Details" so it fits
beside "Schedule" on a phone.
…eader

Deleting an arrangement was a full-width button under Cancel, competing
with the two actions that confirm or dismiss the dialog. It is now the
same small red button in the top-right corner that the location dialog
already uses.
Location badges follow the design's "badge-medium-light": 12px label,
6px radius, the lightest accent fill, 4/8 padding — and the publisher
cap now carries the visitors icon it is drawn with instead of the
groups one.

Shift status badges drop to the same 12px label, and a taken shift
simply reads "Occupied". The publisher headings in the arrangement
dialog are grey like the other form subheadings.
Every size passed its font size through sx, where the default
body-small-semibold class overruled it — small badges rendered at 14px
instead of the 12px they are drawn at, and big ones at 14px instead of
their own step. Each size now picks its class from the type scale, so
the sizes mean what they say and an explicit className still wins.

Adds a `light` badge for the lightest fill of its colour, and rounds
the small badge to 4px like the rest.

Public witnessing uses it for the location badges (with the visitors
icon the design draws them with) instead of styling them by hand, its
schedule setup switches "every day" with a switch rather than a second
row of tabs, and the shift cells drop a step in the week columns.
Rows that come and go now carry their own id instead of leaning on the
array index: partner names in the arrangement dialog, and shifts while
a location is being edited (the id is dropped again on save, so nothing
new reaches the database).

Chained ternaries become named helpers — the slot status, the
arrangement mode, the week header and month count colours, the days a
shift edit touches — and the slot derivation lost two levels of nesting
by moving the seeker rule and the status decision out of the loop.
Badge resolved its text and background colours in two branching
closures inside the component; they are now flat guard chains at module
scope, taking the colour props they need.

The shift cell splits its two layouts into their own components, and
the arrangement dialog's publisher form comes out of a nested JSX
ternary into a small function.
src/components/public_witnessing_card held place and time cards written
before this feature; the shift cells replaced them and nothing imported
them but the components barrel. They also carried fifteen of the repo's
type errors and an untranslated string. Removing them takes the repo
from 3216 to 3201 tsc errors.

Also drops the arrangements bulk save, which nothing calls — unlike the
locations one, which the reorder dialog uses.

An expanded week cell no longer stretches the other cells in its row,
and the "occupied" state loses its badge in the day and week views; in
the week columns the tooltip still names it after two seconds.
The maximum publishers per shift was optional, with every consumer
falling back to three. It is now part of a location like its name and
schedule: the form will not save without it, and the shift capacity
reads it directly.

Week cells reserve the height the expand chevron needs, so a shift with
publishers no longer sits taller than a free one beside it, and the
schedule tab drops the "select a day" placeholder.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (3)
src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx (1)

130-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The default shift capacity of 3 is hard-coded in two hooks. Slot status derivation and seat math read the same fallback independently, so a later change to one side produces inconsistent capacity behavior.

  • src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx#L130-L130: read the fallback from a shared exported constant.
  • src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx#L90-L90: read the same constant and keep the existing comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx` at
line 130, The default shift capacity is duplicated across the two hooks and must
come from one shared exported constant. In
src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx lines
130-130, replace the fallback in the capacity calculation with that constant; in
src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx
lines 90-90, use the same constant while preserving the existing comment.
src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx (1)

199-223: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Report ICS generation failures to the user.

Handle the createEvent callback error with displaySnackNotification, matching handleSave, so failed ICS generation does not close the dialog or silently log only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx`
around lines 199 - 223, Update the createEvent error branch in
handleDownloadCalendar to call displaySnackNotification with the same failure
notification behavior used by handleSave, while preserving the early return so
the dialog remains open and removing the console-only handling.
src/features/ministry/public_witnessing/index.tsx (1)

55-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate selectedLocation derivation across two files. Both files independently compute locations.find(record => record.location_uid === selected) from the same two atoms, with slightly different null/undefined fallbacks.

  • src/features/ministry/public_witnessing/index.tsx#L55-L57: replace this inline .find() with a shared derived atom or hook that both files consume.
  • src/pages/ministry/public_witnessing/usePublicWitnessing.tsx#L18-L22: replace this inline .find() ?? null with the same shared derived atom or hook, so both files stay in sync and share one fallback convention.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/ministry/public_witnessing/index.tsx` around lines 55 - 57,
Replace the duplicated selectedLocation derivation with one shared derived atom
or hook based on the locations and selected atoms. Update
src/features/ministry/public_witnessing/index.tsx lines 55-57 and
src/pages/ministry/public_witnessing/usePublicWitnessing.tsx lines 18-22 to
consume it, using one consistent null/undefined fallback convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/tab_switcher/index.tsx`:
- Around line 52-58: Update the tablist and its ButtonBase tabs to implement
roving keyboard focus: handle ArrowLeft/ArrowRight to move between tabs,
Home/End to focus the first/last tab, and maintain appropriate tabIndex so only
the active tab is in the Tab order. Preserve disabled-tab behavior and invoke
onChange when keyboard navigation selects a new tab.

In `@src/definition/public_witnessing.ts`:
- Around line 4-54: Update the schedule-edit flow that persists
PublicWitnessingShiftType entries to detect arrangements matching an affected
location, date, and original start_time/end_time before saving changed times.
Either warn and migrate those arrangements to the new shift time, or block the
save while existing arrangements remain; ensure bookings stay visible and avoid
relying on a shift ID, since PublicWitnessingShiftType has none.

In `@src/features/ministry/public_witnessing/arrangement_form/index.tsx`:
- Around line 66-74: Update handleConfirmClick to guard against concurrent
submissions by tracking an in-flight save and returning immediately when one is
already running; set the guard before awaiting handleConfirm and clear it after
completion, including failure paths. Ensure the Confirm button uses this state
to remain disabled while the save is running, preventing duplicate arrangement
records.

In `@src/features/ministry/public_witnessing/index.tsx`:
- Around line 38-53: Update the effect containing the locationId branch to
validate that locationId exists in locations before setting selected; when it is
missing, clear or reset the selection and navigate to the public-witnessing list
route, while preserving the existing valid-location behavior and laptop
navigation.

In `@src/features/ministry/public_witnessing/location_delete/index.tsx`:
- Around line 16-20: Update handleDelete to remove or reassign all future public
witnessing arrangements referencing the location’s location_uid before calling
dbPublicWitnessingLocationsSave. Reuse the existing arrangements database
operations and preserve the current soft-delete behavior after those bookings
are handled.

In `@src/features/ministry/public_witnessing/location_form/index.tsx`:
- Around line 258-299: Update the dayRow control to be keyboard-operable by
adding an appropriate interactive role, tabIndex, and keyboard handler that
invokes handleDayRowClick for Enter and Space while preserving the existing
click behavior. Ensure keyboard activation is limited to approved expandable
days and does not interfere with the nested dayCheckbox control.

In `@src/features/ministry/public_witnessing/location_form/useLocationForm.tsx`:
- Around line 27-75: The useLocationForm state is initialized only once, so
changing location leaves fields, shiftsByDay, approvedDays, selectedDay, and
scheduleMode from the previous location. Reset all location-derived state when
the location identity changes, or key the consuming LocationForm by a stable
location identifier so useLocationForm remounts; preserve the existing
empty/default initialization behavior.
- Around line 156-195: Update handleSave to validate every shift in shiftsByDay
before constructing and persisting schedule, requiring each end_time to be
strictly later than its start_time. Reject the save when any shift has an equal
or earlier end time, preserving the existing save flow for valid schedules and
using the form’s established validation or notification mechanism to report the
invalid range.

In
`@src/features/ministry/public_witnessing/locations_reorder/useLocationsReorder.tsx`:
- Around line 20-45: Update the locations_sorted useMemo to detect whether each
location’s findIndex differs from its existing location_data.sort_index before
changing metadata. Skip unchanged locations entirely, or at minimum preserve
their existing updatedAt and avoid saving them; only assign the new sort_index
and current timestamp for reordered locations before
dbPublicWitnessingLocationsBulkSave.

In `@src/features/ministry/public_witnessing/shifts_card/shift_cell.tsx`:
- Around line 123-133: Add the tr_shiftAvailable translation key to every
non-English ministry.json locale file that lacks it, using the locale’s existing
translation conventions and wording. Preserve the statusLabels mapping in
shift_cell.tsx and ensure all locales resolve the available shift label without
exposing the raw key.
- Around line 187-204: Update the expand IconButton and the surrounding outer
button semantics in the shift cell: use an existing key that names the expanded
publisher-list content for the IconButton aria-label, and prevent Enter/Space
events handled by the nested toggle from reaching the outer Box onKeyDown. Also
avoid appending an empty statusLabel to the outer aria-label for past slots,
preserving a valid label without a trailing comma.

In `@src/services/dexie/metadata.ts`:
- Around line 30-31: Update metadata initialization around the defaults
containing public_witnessing_locations and public_witnessing_arrangements so
existing id: 1 records receive these missing keys with the current defaults,
while preserving any existing values. Ensure this migration runs before metadata
consumers access the record, either by merging defaults during initialization or
through a version 13 Dexie upgrade.

In `@src/services/dexie/public_witnessing_arrangements.ts`:
- Line 1: Update dbPublicWitnessingArrangementsClear and
dbPublicWitnessingLocationsClear to call their respective metadata-update
helpers after completing the bulk soft-delete, ensuring both clear mutations are
flagged for synchronization like other writes.
- Line 1: Wrap the record writes and sync-flag metadata updates in both service
save flows with a single appDb.transaction using the corresponding table and
appDb.metadata. Update the put/bulkPut paths in public_witnessing_arrangements
and public_witnessing_locations so each transaction invokes its matching
metadata helper before completing.
- Around line 28-36: Update dbPublicWitnessingArrangementsClear to call
dbUpdatePublicWitnessingArrangementsMetadata() after bulkPut completes, ensuring
the mass soft-delete is marked with send_local: true for synchronization;
preserve the existing early return and soft-delete behavior.
- Around line 14-26: Wrap the arrangement write and metadata update in a single
Dexie read-write transaction in both dbPublicWitnessingArrangementsSave and
dbPublicWitnessingArrangementsBulkSave, ensuring the corresponding put or
bulkPut and dbUpdatePublicWitnessingArrangementsMetadata execute within the same
transaction and commit or fail together.

In `@src/services/dexie/public_witnessing_locations.ts`:
- Around line 28-36: Update dbPublicWitnessingLocationsClear to mark the mass
deletion for synchronization by calling
dbUpdatePublicWitnessingLocationsMetadata() with send_local enabled after
bulkPut completes, matching the metadata update behavior used by other mutations
in the file.
- Around line 14-26: Wrap the writes in both dbPublicWitnessingLocationsSave and
dbPublicWitnessingLocationsBulkSave in a single Dexie transaction that includes
the location put/bulkPut and dbUpdatePublicWitnessingLocationsMetadata call,
ensuring the location and metadata update commit or fail together.

---

Nitpick comments:
In
`@src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx`:
- Around line 199-223: Update the createEvent error branch in
handleDownloadCalendar to call displaySnackNotification with the same failure
notification behavior used by handleSave, while preserving the early return so
the dialog remains open and removing the console-only handling.

In `@src/features/ministry/public_witnessing/index.tsx`:
- Around line 55-57: Replace the duplicated selectedLocation derivation with one
shared derived atom or hook based on the locations and selected atoms. Update
src/features/ministry/public_witnessing/index.tsx lines 55-57 and
src/pages/ministry/public_witnessing/usePublicWitnessing.tsx lines 18-22 to
consume it, using one consistent null/undefined fallback convention.

In `@src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx`:
- Line 130: The default shift capacity is duplicated across the two hooks and
must come from one shared exported constant. In
src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx lines
130-130, replace the fallback in the capacity calculation with that constant; in
src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx
lines 90-90, use the same constant while preserving the existing comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 61fdea1c-6519-45a8-80d1-705c0b6ecc68

📥 Commits

Reviewing files that changed from the base of the PR and between 0025278 and 639a15a.

⛔ Files ignored due to path filters (2)
  • src/locales/en/general.json is excluded by !**/*.json
  • src/locales/en/ministry.json is excluded by !**/*.json
📒 Files selected for processing (50)
  • src/App.tsx
  • src/components/badge/index.tsx
  • src/components/badge/index.types.ts
  • src/components/icons/IconReorder.tsx
  • src/components/month_calendar/index.tsx
  • src/components/month_calendar/index.types.ts
  • src/components/settings_tab/index.styles.tsx
  • src/components/settings_tab/index.tsx
  • src/components/tab_switcher/index.tsx
  • src/components/tab_switcher/index.types.ts
  • src/definition/public_witnessing.ts
  • src/features/ministry/public_witnessing/arrangement_form/index.tsx
  • src/features/ministry/public_witnessing/arrangement_form/index.types.ts
  • src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx
  • src/features/ministry/public_witnessing/index.tsx
  • src/features/ministry/public_witnessing/location_delete/index.tsx
  • src/features/ministry/public_witnessing/location_delete/index.types.ts
  • src/features/ministry/public_witnessing/location_details/index.tsx
  • src/features/ministry/public_witnessing/location_details/index.types.ts
  • src/features/ministry/public_witnessing/location_form/index.tsx
  • src/features/ministry/public_witnessing/location_form/index.types.ts
  • src/features/ministry/public_witnessing/location_form/useLocationForm.tsx
  • src/features/ministry/public_witnessing/locations_list/index.tsx
  • src/features/ministry/public_witnessing/locations_list/index.types.ts
  • src/features/ministry/public_witnessing/locations_reorder/index.tsx
  • src/features/ministry/public_witnessing/locations_reorder/index.types.ts
  • src/features/ministry/public_witnessing/locations_reorder/useLocationsReorder.tsx
  • src/features/ministry/public_witnessing/shifts_card/day_view.tsx
  • src/features/ministry/public_witnessing/shifts_card/index.tsx
  • src/features/ministry/public_witnessing/shifts_card/index.types.ts
  • src/features/ministry/public_witnessing/shifts_card/month_view.tsx
  • src/features/ministry/public_witnessing/shifts_card/shift_cell.tsx
  • src/features/ministry/public_witnessing/shifts_card/shifts_empty.tsx
  • src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx
  • src/features/ministry/public_witnessing/shifts_card/week_view.tsx
  • src/features/ministry/public_witnessing/usePermissions.tsx
  • src/indexedDb/appDb.ts
  • src/indexedDb/tables/public_witnessing_arrangements.ts
  • src/indexedDb/tables/public_witnessing_locations.ts
  • src/layouts/navbar/index.tsx
  • src/pages/dashboard/ministry/index.tsx
  • src/pages/ministry/public_witnessing/index.tsx
  • src/pages/ministry/public_witnessing/usePublicWitnessing.tsx
  • src/services/dexie/metadata.ts
  • src/services/dexie/public_witnessing_arrangements.ts
  • src/services/dexie/public_witnessing_locations.ts
  • src/states/public_witnessing.ts
  • src/utils/date.ts
  • src/wrapper/database_indexeddb/index.tsx
  • src/wrapper/database_indexeddb/useIndexedDb.tsx

Comment on lines +52 to +58
<ButtonBase
key={option.value}
role="tab"
aria-selected={isActive}
disabled={option.disabled}
disableRipple
onClick={() => onChange(option.value)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find target file:"
fd -a 'index.tsx$' . | sed 's#^\./##' | rg 'src/components/tab_switcher/index.tsx|tab_switcher/index.tsx' || true

echo
echo "Target outline:"
ast-grep outline src/components/tab_switcher/index.tsx || true

echo
echo "Target relevant lines:"
cat -n src/components/tab_switcher/index.tsx | sed -n '1,140p'

echo
echo "Search for keyboard handling in related files:"
rg -n "ArrowRight|ArrowLeft|ArrowUp|ArrowDown|Home|End|key=|onKeyDown|onKeyUp|onKeyPress|tabIndex|role=\"tablist\"|role=\\\"tab\\\"" src/components/tab_switcher src || true

Repository: sws2apps/organized-app

Length of output: 50378


Implement keyboard navigation for the tablist.

role="tablist" with role="tab" items must expose roving Arrow/Home/End focus. The current ButtonBase tab only supports Tab navigation, so add tab-keyboard handling or use a control whose semantics match this interaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/tab_switcher/index.tsx` around lines 52 - 58, Update the
tablist and its ButtonBase tabs to implement roving keyboard focus: handle
ArrowLeft/ArrowRight to move between tabs, Home/End to focus the first/last tab,
and maintain appropriate tabIndex so only the active tab is in the Tab order.
Preserve disabled-tab behavior and invoke onChange when keyboard navigation
selects a new tab.

Comment on lines +4 to +54
export type PublicWitnessingShiftType = {
start_time: string; // "HH:mm"
end_time: string; // "HH:mm"
};

export type PublicWitnessingDayScheduleType = {
weekday: number; // 1 (Monday) – 7 (Sunday)
shifts: PublicWitnessingShiftType[];
};

export type PublicWitnessingLocationType = {
location_uid: string;
location_data: {
_deleted: boolean;
updatedAt: string;
name: string;
address: string;
cart_stored_at: string;
// Optional — when unset, shifts fall back to the default capacity of 3.
max_publishers?: number;
description: string;
sort_index: number;
schedule: PublicWitnessingDayScheduleType[];
};
};

export type PublicWitnessingPublisherType = {
name: string;
// Set when the publisher is a congregation person; free-text partners
// (non-app users) have a name only.
person_uid?: string;
};

export type PublicWitnessingArrangementType = {
arrangement_uid: string;
arrangement_data: {
_deleted: boolean;
updatedAt: string;
location_uid: string;
date: string; // "yyyy/MM/dd"
start_time: string; // "HH:mm"
end_time: string; // "HH:mm"
// person_uid of the author — only the author (and admins) may edit.
created_by: string;
partner_needed: boolean;
// Publishers still wanted when partner_needed; the shift stays joinable
// until this many partners have been arranged.
partner_count?: number;
publishers: PublicWitnessingPublisherType[];
};
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect how shifts are derived from schedule + arrangements,
# and whether shift-time edits account for existing arrangement records.
fd useShiftsCard.tsx | xargs -I{} cat -n {}
rg -n 'start_time|end_time|schedule' src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx 2>/dev/null
rg -n 'schedule' src/features/ministry/public_witnessing/location_form -A5 -B5

Repository: sws2apps/organized-app

Length of output: 22238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## relevant location form sections\n'
sed -n '280,380p' src/features/ministry/public_witnessing/location_form/index.tsx | cat -n
printf '\n## shifts editor references\n'
rg -n 'selectedShifts|setSelectedShifts|shiftsEditor|Start|End|save|handleSave' src/features/ministry/public_witnessing/location_form -C 3

printf '\n## savedLocation usages and validation around public witnessing\n'
rg -n 'savePublicWitnessing|publicWitnessing|_deleted|location_data|save.*location|handleSave' src -g '!**/node_modules/**' | head -200

Repository: sws2apps/organized-app

Length of output: 27686


Preserve existing bookings when schedule times change.

useShiftsCard builds arrangements from the date | start_time key and maps them into derived shifts. Locations save edited schedule shifts without comparing changed start_time/end_time values against arrangements, and PublicWitnessingShiftType has no stable id. A changed shift can leave bookings for the old time and omit that time from generated slots, so publishers lose visibility for the shifted arrangement. Add the change warning/migration step or prevent saving shifts that have existing arrangements for the affected date/time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/definition/public_witnessing.ts` around lines 4 - 54, Update the
schedule-edit flow that persists PublicWitnessingShiftType entries to detect
arrangements matching an affected location, date, and original
start_time/end_time before saving changed times. Either warn and migrate those
arrangements to the new shift time, or block the save while existing
arrangements remain; ensure bookings stay visible and avoid relying on a shift
ID, since PublicWitnessingShiftType has none.

Comment on lines +66 to +74
const handleConfirmClick = async () => {
const saved = await handleConfirm();
if (!saved) return;
if (mode === 'edit') {
props.onClose();
return;
}
setStep('calendar');
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Block repeated submits of the Confirm button.

handleConfirmClick is async and nothing disables the button while the save runs. In create mode, handleSave generates a new arrangement_uid on each call, so a double click writes two arrangement records for the same slot. Each duplicate consumes a seat and only its author can remove it.

🐛 Proposed fix
   const [step, setStep] = useState<'form' | 'calendar'>('form');
+  const [isSaving, setIsSaving] = useState(false);
   const handleConfirmClick = async () => {
-    const saved = await handleConfirm();
-    if (!saved) return;
+    if (isSaving) return;
+    setIsSaving(true);
+    try {
+      const saved = await handleConfirm();
+      if (!saved) return;
+    } finally {
+      setIsSaving(false);
+    }
     if (mode === 'edit') {
       props.onClose();
       return;
     }
     setStep('calendar');
   };
-            <Button variant="main" onClick={handleConfirmClick}>
+            <Button
+              variant="main"
+              disabled={isSaving}
+              onClick={handleConfirmClick}
+            >
               {t('tr_confirm')}
             </Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/ministry/public_witnessing/arrangement_form/index.tsx` around
lines 66 - 74, Update handleConfirmClick to guard against concurrent submissions
by tracking an in-flight save and returning immediately when one is already
running; set the guard before awaiting handleConfirm and clear it after
completion, including failure paths. Ensure the Confirm button uses this state
to remain disabled while the save is running, preventing duplicate arrangement
records.

Comment on lines +38 to +53
useEffect(() => {
if (locationId) {
setSelected(locationId);

if (laptopUp) navigate('/public-witnessing', { replace: true });

return;
}

const isValid = locations.some(
(record) => record.location_uid === selected
);
if (!isValid) {
setSelected(locations.at(0)?.location_uid ?? null);
}
}, [locationId, locations, selected, setSelected, laptopUp, navigate]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Deleting the currently-viewed subpage location leaves a blank screen.

The if (locationId) branch sets selected and returns early without checking that locationId still refers to an existing location. The else branch does this check (locations.some(...)), but the locationId branch does not.

If a user deletes the location shown on /public-witnessing/:locationId (through location_delete/index.tsx's handleDelete, which never navigates away), the app stays on that URL. On the next render, selected is re-set to the now-deleted locationId, selectedLocation becomes undefined once the location is filtered out of locations, and the mobile view renders neither the location details nor LocationsList, leaving a blank page with no recovery path other than manually changing the URL.

Validate locationId against locations and navigate back to the list when it no longer exists.

🐛 Proposed fix to validate `locationId` and fall back to the list
   useEffect(() => {
     if (locationId) {
-      setSelected(locationId);
+      const isValidLocationId = locations.some(
+        (record) => record.location_uid === locationId
+      );
+
+      if (!isValidLocationId) {
+        navigate('/public-witnessing', { replace: true });
+        return;
+      }
+
+      setSelected(locationId);

       if (laptopUp) navigate('/public-witnessing', { replace: true });

       return;
     }

     const isValid = locations.some(
       (record) => record.location_uid === selected
     );
     if (!isValid) {
       setSelected(locations.at(0)?.location_uid ?? null);
     }
   }, [locationId, locations, selected, setSelected, laptopUp, navigate]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (locationId) {
setSelected(locationId);
if (laptopUp) navigate('/public-witnessing', { replace: true });
return;
}
const isValid = locations.some(
(record) => record.location_uid === selected
);
if (!isValid) {
setSelected(locations.at(0)?.location_uid ?? null);
}
}, [locationId, locations, selected, setSelected, laptopUp, navigate]);
useEffect(() => {
if (locationId) {
const isValidLocationId = locations.some(
(record) => record.location_uid === locationId
);
if (!isValidLocationId) {
navigate('/public-witnessing', { replace: true });
return;
}
setSelected(locationId);
if (laptopUp) navigate('/public-witnessing', { replace: true });
return;
}
const isValid = locations.some(
(record) => record.location_uid === selected
);
if (!isValid) {
setSelected(locations.at(0)?.location_uid ?? null);
}
}, [locationId, locations, selected, setSelected, laptopUp, navigate]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/ministry/public_witnessing/index.tsx` around lines 38 - 53,
Update the effect containing the locationId branch to validate that locationId
exists in locations before setting selected; when it is missing, clear or reset
the selection and navigate to the public-witnessing list route, while preserving
the existing valid-location behavior and laptop navigation.

Comment on lines +16 to +20
const record = structuredClone(location);
record.location_data._deleted = true;
record.location_data.updatedAt = new Date().toISOString();

await dbPublicWitnessingLocationsSave(record);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether location deletion cascades to or is checked against arrangements.
set -euo pipefail

rg -n -C 5 'location_uid' src/services/dexie/public_witnessing_arrangements.ts 2>/dev/null || true
rg -n -C 5 '_deleted' src/services/dexie/public_witnessing_locations.ts 2>/dev/null || true
rg -n -C 3 'location_uid' src/features/ministry/public_witnessing --glob '*.tsx' --glob '*.ts' | rg -v 'location_delete'

Repository: sws2apps/organized-app

Length of output: 8674


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant file sizes =="
wc -l \
  src/services/dexie/public_witnessing_arrangements.ts \
  src/services/dexie/public_witnessing_locations.ts \
  src/features/ministry/public_witnessing/location_delete/index.tsx \
  src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx \
  src/features/ministry/public_witnessing/index.tsx \
  src/features/ministry/public_witnessing/arrangement_form/useArrangementForm.tsx \
  src/features/ministry/public_witnessing/locations_list/index.tsx \
  2>/dev/null || true

echo
echo "== deletion service =="
sed -n '1,60p' src/services/dexie/public_witnessing_locations.ts
sed -n '1,180p' src/services/dexie/public_witnessing_arrangements.ts

echo
echo "== delete location component =="
sed -n '1,140p' src/features/ministry/public_witnessing/location_delete/index.tsx

echo
echo "== location selection/filter =="
sed -n '1,120p' src/features/ministry/public_witnessing/index.tsx
sed -n '90,130p' src/features/ministry/public_witnessing/shifts_card/useShiftsCard.tsx

echo
echo "== search deleted filters and cancellation =="
rg -n '_deleted|_deleted\.|deleted|cancel|cancelled|cancel_arrangement|arrangement_data\._deleted|_deleted\s*is|_deleted\s*==|_deleted\s*!' \
  src/features/ministry/public_witnessing src/services/dexie \
  --glob '*.ts' --glob '*.tsx' \
  | head -200

Repository: sws2apps/organized-app

Length of output: 19072


Cancel arrangements when deleting a location.

handleDelete only soft-deletes the location, while arrangements continue to store location_uid and remain in public_witnessing_arrangements. If a location with bookings is hidden from the lists, those bookings can become unreachable. Delete or reassign future arrangements before calling dbPublicWitnessingLocationsSave.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/ministry/public_witnessing/location_delete/index.tsx` around
lines 16 - 20, Update handleDelete to remove or reassign all future public
witnessing arrangements referencing the location’s location_uid before calling
dbPublicWitnessingLocationsSave. Reuse the existing arrangements database
operations and preserve the current soft-delete behavior after those bookings
are handled.

@@ -0,0 +1,36 @@
import appDb from '@db/appDb';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear soft-deletes records but never flags them for sync, in both dexie services. Every other mutation in these two files calls its metadata-update helper after writing so the local-sync layer knows to push the change. Clear skips this call in both files, so mass local deletions have no signal to be sent to the server until an unrelated write later sets the flag.

  • src/services/dexie/public_witnessing_arrangements.ts#L28-36: call dbUpdatePublicWitnessingArrangementsMetadata() after the bulk soft-delete in dbPublicWitnessingArrangementsClear, or confirm this function is intentionally local-only.
  • src/services/dexie/public_witnessing_locations.ts#L28-36: call dbUpdatePublicWitnessingLocationsMetadata() after the bulk soft-delete in dbPublicWitnessingLocationsClear, or confirm this function is intentionally local-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/dexie/public_witnessing_arrangements.ts` at line 1, Update
dbPublicWitnessingArrangementsClear and dbPublicWitnessingLocationsClear to call
their respective metadata-update helpers after completing the bulk soft-delete,
ensuring both clear mutations are flagged for synchronization like other writes.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Save/BulkSave writes aren't atomic with the sync-flag update, in both dexie services. In each file, the record write and the metadata write that sets send_local: true are two separate, independently-committed Dexie operations. A failure or app close between them leaves the record persisted without the sync flag set.

  • src/services/dexie/public_witnessing_arrangements.ts#L14-26: wrap appDb.public_witnessing_arrangements.put/bulkPut and dbUpdatePublicWitnessingArrangementsMetadata() in one appDb.transaction('rw', [appDb.public_witnessing_arrangements, appDb.metadata], ...) call.
  • src/services/dexie/public_witnessing_locations.ts#L14-26: wrap appDb.public_witnessing_locations.put/bulkPut and dbUpdatePublicWitnessingLocationsMetadata() in one appDb.transaction('rw', [appDb.public_witnessing_locations, appDb.metadata], ...) call.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/dexie/public_witnessing_arrangements.ts` at line 1, Wrap the
record writes and sync-flag metadata updates in both service save flows with a
single appDb.transaction using the corresponding table and appDb.metadata.
Update the put/bulkPut paths in public_witnessing_arrangements and
public_witnessing_locations so each transaction invokes its matching metadata
helper before completing.

Comment thread src/services/dexie/public_witnessing_arrangements.ts Outdated
Comment on lines +28 to +36
export const dbPublicWitnessingArrangementsClear = async () => {
const records = await appDb.public_witnessing_arrangements.toArray();
if (records.length === 0) return;
for (const record of records) {
record.arrangement_data._deleted = true;
record.arrangement_data.updatedAt = new Date().toISOString();
}
await appDb.public_witnessing_arrangements.bulkPut(records);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear never marks arrangements for sync.

dbPublicWitnessingArrangementsClear soft-deletes every arrangement (_deleted = true, refreshed updatedAt) but never calls dbUpdatePublicWitnessingArrangementsMetadata(). Every other mutation in this file sets send_local: true after writing. Without it here, a mass local deletion has no way to be picked up for sync until an unrelated write later flips the flag.

Call the metadata update after the bulk soft-delete, unless this function is intentionally local-only (e.g., a pre-resync cache wipe that must not be pushed back to the server).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/dexie/public_witnessing_arrangements.ts` around lines 28 - 36,
Update dbPublicWitnessingArrangementsClear to call
dbUpdatePublicWitnessingArrangementsMetadata() after bulkPut completes, ensuring
the mass soft-delete is marked with send_local: true for synchronization;
preserve the existing early return and soft-delete behavior.

Comment on lines +14 to +26
export const dbPublicWitnessingLocationsSave = async (
location: PublicWitnessingLocationType
) => {
await appDb.public_witnessing_locations.put(location);
await dbUpdatePublicWitnessingLocationsMetadata();
};

export const dbPublicWitnessingLocationsBulkSave = async (
locations: PublicWitnessingLocationType[]
) => {
await appDb.public_witnessing_locations.bulkPut(locations);
await dbUpdatePublicWitnessingLocationsMetadata();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Wrap the record write and metadata update in one transaction.

dbPublicWitnessingLocationsSave and dbPublicWitnessingLocationsBulkSave each perform two separate, independently-committed writes: the location write, then the metadata write that sets send_local: true. If the app closes or an error occurs between the two awaits, the location persists locally, but the sync flag never gets set.

Wrap both writes in a single Dexie transaction so they commit or fail together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/dexie/public_witnessing_locations.ts` around lines 14 - 26, Wrap
the writes in both dbPublicWitnessingLocationsSave and
dbPublicWitnessingLocationsBulkSave in a single Dexie transaction that includes
the location put/bulkPut and dbUpdatePublicWitnessingLocationsMetadata call,
ensuring the location and metadata update commit or fail together.

Comment on lines +28 to +36
export const dbPublicWitnessingLocationsClear = async () => {
const records = await appDb.public_witnessing_locations.toArray();
if (records.length === 0) return;
for (const record of records) {
record.location_data._deleted = true;
record.location_data.updatedAt = new Date().toISOString();
}
await appDb.public_witnessing_locations.bulkPut(records);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear never marks locations for sync.

dbPublicWitnessingLocationsClear soft-deletes every location (_deleted = true, refreshed updatedAt) but never calls dbUpdatePublicWitnessingLocationsMetadata(). Every other mutation in this file sets send_local: true after writing. Without it here, a mass local deletion has no way to be picked up for sync until an unrelated write later flips the flag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/dexie/public_witnessing_locations.ts` around lines 28 - 36,
Update dbPublicWitnessingLocationsClear to mark the mass deletion for
synchronization by calling dbUpdatePublicWitnessingLocationsMetadata() with
send_local enabled after bulkPut completes, matching the metadata update
behavior used by other mutations in the file.

The vertical divider only appeared once a day was open, so the empty
editor lost its column. It stays now, and the prompt beside it is back
as "Select a day to plan a shift".
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant