Skip to content

feat(persons): support middle names and a separate print name format - #5313

Open
ux-git wants to merge 22 commits into
sws2apps:mainfrom
ux-git:feat/middle-name-support
Open

feat(persons): support middle names and a separate print name format#5313
ux-git wants to merge 22 commits into
sws2apps:mainfrom
ux-git:feat/middle-name-support

Conversation

@ux-git

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

Copy link
Copy Markdown
Member

Description

Adds middle name support for persons and a separate name format for printed forms, so congregations can keep short names on screen and in schedules while printing full legal names on official forms.

Fixes #4448

Data model

  • New optional person_middlename field on PersonType and VisitingSpeakerType, middlename on user_settings and on cong_settings.circuit_overseer, seeded in every schema literal (including the person literal in states/persons.ts).
  • New optional congregation setting cong_settings.print_fullname_option, stored per data view with the same record shape as fullname_option. The existing fullname_option key is not renamed.
  • TABLE_ENCRYPTION_MAP updated: person_middlename (persons, visiting_speakers, shared), middlename (user settings, public), print_fullname_option (shared).

Name composition

  • FullnameOption gains four members: FIRST_MIDDLE_LAST, LAST_FIRST_MIDDLE, LAST_COMMA_FIRST, LAST_COMMA_FIRST_MIDDLE. The two existing options never read the middle name, so every current congregation keeps byte-identical output until it picks a new format.
  • The comma variants are separate options on purpose: comma-free last-first is the natural order for languages such as Japanese and Hungarian (the ones LANGUAGE_LIST already defaults to LAST_BEFORE_FIRST), where a comma would be wrong, while "Gonzalez, William" is the western sorted-list convention requested in the issue.
  • buildPersonFullname accepts the middle name as a fourth optional parameter and every direct call site passes it. personGetDisplayName, speakerGetDisplayName, personGetFullname and usePerson().getName read it internally, which covers their callers without touching them.
  • generateDisplayName is unchanged: display names stay compact and never include the middle name.
  • The circuit overseer and incoming speaker editors keep a fixed First/Middle/Last field order in a wrapping row (same pattern as the person editor) instead of reversing the row per format, because reversing a three-field row would produce Last Middle First, which no locale uses.

Entry UI

  • A Middle name input (always visible, always optional) in: person Basic information, My profile, the initial setup person record, the incoming speaker editor and the circuit overseer settings. No format makes the field required; when it is left blank the composed name simply omits it. The three-field name rows use flexWrap: 'wrap' with flex: '1 0 200px' per field, following the congregation create form pattern, so they reflow without breakpoint hooks.
  • The circuit overseer name row moves from the reversing TwoColumnsRow to the same fixed-order wrapping row as the person editor, because reversing a three-field row would render Last Middle First, which is not a real name order. His middle name flows through COFullnameState into schedules and the person selector with the congregation's format.
  • Persons search also matches the middle name.
  • The persons XLSX export gains a Middle name column and the import maps it back, so export and import round-trip.

Settings UI

  • Name format options render as two rows: the format label, and underneath in the small grey style a live example produced by running buildPersonFullname over three translated sample name parts. The preview therefore always matches real output and cannot drift from the code. This reuses the existing menu item shape from the hours-credit presets (MenuItem with minHeight 56px wrapping a Stack of two Typography lines) and renderValue so the collapsed field shows only the label.
  • A second select, "Name format for printed forms", sits next to the existing one on the meeting forms card. Both are one shared NameFormatSelect component and one shared persistence hook parameterised by settings field. The print select leads with a "Same as in-app format" entry, selected by default; choosing it tombstones the stored print record so the fallback applies again, and choosing a concrete format revives it.
  • printFullnameOptionState falls back to the in-app format whenever no print format is stored. No migration is needed and printed output does not change until someone chooses a print format. For the same reason the schema deliberately does not seed a default record; seeding one would freeze new congregations on First Last even after they change the screen format.
  • Deleting a language group tombstones its print_fullname_option record the same way it already tombstones fullname_option.
  • The language switcher used to overwrite fullname_option with the language default on every UI language change; it now maps the language's first-or-last ordering onto the congregation's middle-name and comma choice instead of discarding it.

Print paths

The document builders now read printFullnameOptionState: the midweek (S-140, S-89), weekend and outgoing talks builders in services/app/schedules.ts, the S-21 publisher card hook and the field service groups export. The circuit overseer name on CO visit weeks goes through new COPrintFullnameState / COPrintScheduleNameState atoms, and the default Watchtower study conductor fallback name follows the print format as well, so every printed name obeys the same setting. All of these are export-only code paths (every caller of schedulesWeekGetAssigned sits inside the three builders, which are reached only from the midweek and weekend export hooks), so on-screen rendering is unaffected.

Sync

  • user_settings.middlename is included in the backup worker payload and restored on sign-in only when the server actually sends it, so a missing backend field cannot clear a locally entered value.
  • The outgoing speakers payload copies person_middlename from the linked person record, next to the existing first and last name overrides.
  • Dependency: the backend needs to accept person_middlename (persons, visiting speakers), user_settings.middlename and cong_settings.print_fullname_option. All keys are additive; nothing existing is renamed or removed.

Localization

New keys in en: tr_middlename (onboarding), and in congregation: tr_printNameFormat, tr_nameFormatSameAsApp, tr_nameFormatFirstLast, tr_nameFormatLastFirst, tr_nameFormatFirstMiddleLast, tr_nameFormatLastFirstMiddle, tr_nameFormatLastCommaFirst, tr_nameFormatLastCommaFirstMiddle, and the three sample name parts tr_nameFormatSampleFirstname, tr_nameFormatSampleMiddlename, tr_nameFormatSampleLastname. The legacy tr_formatFirstLast and tr_formatLastFirst strings were left untouched because their translations embed example names; they become unused once this lands and can be retired after the new keys are sourced in Crowdin.

Deliberate limits

  • The congregation access screens compose names from the user profile served by the API, which has no middle name yet; they are unchanged until the backend provides one.
  • TEST mode seeds gender-matched middle names for three quarters of the dummy persons, and leaves every fourth person without one, so both shapes stay visible with demo data.

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
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules

ux-git added 15 commits July 25, 2026 22:13
Introduce an optional middle name across every record that carries a
person's name:

- PersonType.person_data.person_middlename
- VisitingSpeakerType.speaker_data.person_middlename
- SettingsType cong_settings.circuit_overseer.middlename
- SettingsType user_settings.middlename

The field is typed optional on purpose. Records arriving from the
backend will not carry it until the server side ships, so declaring it
required would make the type lie about what is actually in IndexedDB.
Reads must stay defensive until that lands.

Seed it in every schema literal, including the standalone person object
in states/persons.ts, so a newly created person and a restored record
have the same shape.

No Dexie version bump: person_middlename is not an index, and an absent
optional field needs no backfill.

Add person_middlename to the persons entry of TABLE_ENCRYPTION_MAP,
matching the 'shared' level already used for first and last name.
visiting_speakers needs no entry there, as it encrypts speaker_data as
a whole.
Add the middle name input to every form that already collects a first
and last name:

- person record (Persons -> Basic information)
- My profile, backed by a new middlenameState atom
- the initial setup wizard

The field is always visible. A middle name is data about a person, not
a display preference, so hiding it behind a name-format setting would
make it impossible to record the name before choosing how to show it.

Display names are deliberately left alone. Their purpose is the compact
form ("B. Mike"), so generateDisplayName keeps its current signature and
middle names never feed into it.

Note that a middle name is not rendered anywhere yet: the two current
name formats are First Last and Last First. Formats that include it are
design work tracked in sws2apps#4448.
The XLSX export gains a Middle name column and the import mapper gains
the matching field, so exporting a congregation and importing it back
no longer drops the value.

changeMiddlename only writes the field. Unlike changeFirstname and
changeLastname it does not touch the display name, which stays the
compact first-plus-last-initials form.
The three name inputs were added to rows that were never designed for a
third child. Adopt the pattern already used by the congregation create
form: flexWrap 'wrap' on the row and flex '1 0 200px' on each field, so
the inputs reflow to 2+1 and then stack on their own, with no breakpoint
hook at all.

That makes nameFlex redundant, and it was dead anyway: it compared
appLang against 'mg-MG', but appLang holds a threeLettersCode ('mlg'),
never a locale, so neither reverse branch could ever run. Remove the
state, the effect and the now unused useBreakpoints in the two other
forms.

Also drop the duplicated middle name writer in useBasicInfo and call
changeMiddlename from @utils/person instead. Its first and last name
siblings are deliberately left alone: unlike the utils versions they
only regenerate the display name while it is still empty.
My profile wrote user_settings.middlename, but nothing carried it off
the device, so it disappeared on the next sign-in:

- include it in the sync payload built by the backup worker
- map it in TABLE_ENCRYPTION_MAP next to firstname and lastname, or the
  key falls through the unmapped-key recursion in the encryption service
- restore it on a fresh device in the three sign-in paths

The sign-in paths only write the field when the server actually sent
one. The backend has no middle name yet, so an unconditional write
would clear a locally entered value on every sign-in. Declaring it
optional on UserLoginResponseType records the contract we need without
pretending it already exists.

Remove cong_settings.circuit_overseer.middlename again. It had no
input, no handler and no reader, and giving the circuit overseer one
means a third field in a TwoColumnsRow shared by six settings screens,
whose row-reverse branch only makes sense with two children. That is a
design decision of its own, so leave the dead field out until it is
taken.
Add FIRST_MIDDLE_LAST and LAST_FIRST_MIDDLE to FullnameOption and teach
buildPersonFullname to place the middle name for them. The middle name
is the fourth parameter: the third is a numeric enum, so appending is
the only order in which a mis-placed argument is a type error rather
than a silent swap.

This is behaviour-neutral. The two existing options never read the new
parameter, so every congregation keeps byte-identical output until
someone picks one of the new formats.

personGetDisplayName, speakerGetDisplayName, personGetFullname and
usePerson's getName now read the middle name themselves, which covers
their callers without touching a single one of them.
Mechanical fourth argument at the remaining direct buildPersonFullname
call sites, plus the logged-in user's own name in fullnameState.

Six sites are deliberately left alone: the app access screens and the
join request item compose a name from user.profile / request, which the
backend serves without a middle name. They will follow once the API
carries one.

Delete personGetScheduleName. It had no callers anywhere in the tree,
so it was migrated by nobody and is better removed than updated.
Each option now renders its name on the first row and a worked example
underneath in the smaller grey style, reusing the menu item shape the
hours-credit presets already use: a MenuItem with minHeight 56px
wrapping a Stack of two Typography lines.

The example is not a translated sentence. It is produced by running
buildPersonFullname over three sample name parts, so it always shows
exactly what the selected format will actually produce and new formats
need no new strings.

New label keys rather than a reword of tr_formatFirstLast and
tr_formatLastFirst: those two carry the example inside the string in
about fifty approved translations, several of them deliberately
localised, and rewording them re-sources the lot.

Select needs renderValue here, otherwise the collapsed field would show
both rows stacked inside the input. Same treatment the source language
selector already uses.
Add cong_settings.print_fullname_option, per data view like its screen
counterpart, and one more Select on the meeting forms card built from
the shared name format select.

printFullnameOptionState falls back to the screen format whenever a
congregation has no print format stored. That keeps printed output
byte-identical for everyone who never opens the new setting, so no
migration is needed and nobody's PDFs change under them.

Also stop the language switcher from discarding a middle name format.
It rewrites fullname_option from the language default on every UI
language change, which would silently undo the choice; it now maps the
language's first-or-last ordering onto the matching middle name variant
instead.
Point the document builders at printFullnameOptionState: the midweek,
weekend and outgoing talks builders in schedules.ts, the S-21 publisher
card and the field service group export.

All of them are print-only. schedulesWeekGetAssigned has twenty-eight
callers and every one sits inside schedulesMidweekData,
schedulesWeekendData or scheduleOutgoingSpeakers, and those three are
reached only from useMidweekExport and useWeekendExport, so nothing on
screen changes.

Because printFullnameOptionState falls back to the screen format until
a congregation picks one, this is a no-op for existing data and only
takes effect once someone chooses a print format.
The circuit overseer and incoming speaker editors decided field order
and the gender icon by comparing against FIRST_BEFORE_LAST, which
misplaces things for the two middle name formats. Route both through a
shared isLastnameFirst helper instead.

Also copy the middle name from the linked person into the outgoing
speakers sync payload, next to the first and last name overrides, so
receiving congregations get it too.
Two thirds of the dummy persons get a middle name from a small fixed
pool, chosen by index so reseeding stays deterministic; every third
person keeps an empty one so both shapes stay visible while testing.
The incoming dummy speaker Ribeiro Gonzaga gets one as well.
"Last First" without a comma and "Last, First" with one are different
conventions. The comma-free options serve languages whose natural order
puts the family name first (Japanese, Hungarian, Malagasy - the same
languages LANGUAGE_LIST already defaults to LAST_BEFORE_FIRST), where a
comma would be wrong. The comma marks the western sorted-list inversion
used on rosters and official forms, and is what issue sws2apps#4448 spells out
as "Last, First".

Keeping them separate also keeps this additive: nobody's existing
choice changes its rendering, which is exactly the mistake an earlier
attempt made when it put a comma into LAST_BEFORE_FIRST for everyone.

The language switcher mapping now preserves both the middle name and
the comma choice, collapsing the comma back to natural order only when
the new language writes the first name first.
useNameFormat and usePrintNameFormat were structural copies of each
other. Both now delegate to a single useNameFormatOption that takes the
settings field, which keeps the per-folder hook files as thin wrappers
and removes the duplicated persistence logic.

Also pick the dummy middle names from gender-matched pools so the demo
data reads naturally.
@vercel

vercel Bot commented Jul 25, 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 26, 2026 7:44pm
test-organized-app Ready Ready Preview Jul 26, 2026 7:44pm

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds optional middle-name fields across person, speaker, and user settings data. It updates fullname construction and display paths, introduces separate print fullname formats, adds middle-name editing and import/export support, and persists the new values through onboarding, profiles, schedules, backups, and settings.

Changes

Middle-name data and persistence

Layer / File(s) Summary
Data contracts and storage
src/definition/*, src/services/dexie/*, src/constants/table_encryption_map.ts, src/services/worker/backupUtils.ts
Adds middle-name and print-format fields to exported types, schemas, encryption mappings, backups, and dummy data.
Editing and synchronization
src/features/dashboard/..., src/features/my_profile/..., src/features/persons/basic_info/..., src/features/congregation/settings/circuit_overseer/..., src/features/persons/speakers_catalog/...
Adds middle-name fields, handlers, persistence, and wrapping layouts to person, profile, circuit-overseer, and speaker forms.

Fullname formatting and propagation

Layer / File(s) Summary
Fullname construction and derived state
src/utils/common.ts, src/states/settings.ts, src/services/app/schedules.ts, src/services/app/persons.ts
Supports middle-name placement, comma-based formats, print-specific options, filtering, sorting, and schedule naming.
Print format controls
src/features/congregation/settings/meeting_forms/*
Adds reusable name-format selection, print-format settings, inheritance behavior, and persistence for congregation-specific print options.
Feature-wide name propagation
src/features/*, src/pages/*, src/services/states/*
Passes optional middle names into fullname construction across groups, applications, assignments, reports, speakers, and person views.
Import and export support
src/features/persons/import_export/*
Adds middle-name import metadata and a middle-name column to person Excel exports.

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

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant NameFormatSelect
  participant useNameFormatOption
  participant Dexie
  participant ScheduleExport
  participant NameBuilder

  Editor->>NameFormatSelect: choose print fullname format
  NameFormatSelect->>useNameFormatOption: submit FullnameOption
  useNameFormatOption->>Dexie: save print_fullname_option
  ScheduleExport->>Dexie: read print format and middle name
  ScheduleExport->>NameBuilder: build formatted name
  NameBuilder-->>ScheduleExport: return printed fullname
Loading

Possibly related PRs

Suggested reviewers: rhahao

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the requested name fields, formatting options, print-specific behavior, and layout improvements.
Out of Scope Changes check ✅ Passed The diff stays focused on name handling, print formatting, and related sync/UI updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: middle-name support plus a separate print name format.
Description check ✅ Passed The description is detailed and directly matches the changeset, covering middle names, print formats, UI, sync, and print paths.
✨ 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 added 2 commits July 26, 2026 12:33
The circuit overseer now follows the congregation name format like
everyone else: an optional Middle name input between First and Last,
composed into COFullnameState so schedules and the person selector
render it with the selected format. Left blank, nothing changes.

The name row switches from the reversing TwoColumnsRow to the same
fixed-order wrapping row the person editor uses, since reversing a
three-field row would produce Last Middle First, which is not a real
name order in any locale.
ux-git added 3 commits July 26, 2026 13:07
The CO visit blocks in the midweek and weekend document builders read
COFullnameState and COScheduleNameState, which compose with the in-app
format, so on a CO visit week the overseer was the only name on the
printed schedule following the screen format. New COPrintFullnameState
and COPrintScheduleNameState mirror their screen counterparts on top of
printFullnameOptionState, and both builders now use them. The screen
progress helpers keep the screen variants.
The visiting speaker type carried person_middlename, but only outgoing
speakers ever set it via the linked person record; for incoming
speakers the field was dead because their editor had no input. Add the
Middle name field and its update handler, mirroring the first name one
minus the display name regeneration, which middle names never feed.

The editor row switches to the fixed-order wrapping pattern used by the
person and circuit overseer editors, since reversing a three-field row
would produce Last Middle First. The gender icon stays on the first
name field. That removes the last isLastnameFirst consumer, so the
helper goes too.
Deleting a language group marked its fullname_option record deleted but
left a chosen print_fullname_option record behind forever. Mirror the
same tombstone block for the print format.

Also document in the schema why print_fullname_option is deliberately
not seeded: while no record exists the atom follows the in-app format,
which is what keeps printed output unchanged for congregations that
never touch the new setting. Seeding a default would freeze new
congregations on First Last even after they switch the screen format.

The TEST congregation circuit overseer gets a middle name so the CO
paths are visible with demo data.
coderabbitai[bot]

This comment was marked as resolved.

@ux-git ux-git changed the title feat: support middle names and a separate print name format feat(persons): support middle names and a separate print name format Jul 26, 2026
ux-git added 2 commits July 26, 2026 21:43
Once a print format was chosen there was no way back to inheriting the
in-app format, because the fallback only applies while no record is
stored. The print select now leads with a "Same as in-app format" entry
that tombstones the stored record instead of writing a value, and
printFullnameOptionState skips tombstoned records so the fallback
applies again. Re-choosing a format revives the tombstoned record.

The sentinel is deliberately not a FullnameOption member, so it can
never be persisted as a real format; the screen select does not render
the entry and is unchanged.
defaultWTStudyConductorNameState composed the fallback conductor name
with the in-app format, but its only consumer is the weekend document
builder, so the printed schedule could disagree with every other name
on the page. Same correction the circuit overseer name already got.
@sonarqubecloud

Copy link
Copy Markdown

@mergify

mergify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

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.

[FEAT] Support for First/Middle/Last Names and separate print name formats

1 participant