feat(profile): first/last name fields + collapse notification preferences

Two related cleanups for the user profile surface area:

(1) Add canonical first_name + last_name columns to user_profiles.
    Migration 0049 backfills from display_name by splitting on the
    first whitespace run; single-token names land as
    (display_name, NULL) so we never throw away existing data.
    Display name becomes an optional override (nicknames, vanity
    formatting). /api/v1/me PATCH now accepts firstName/lastName,
    and the user-settings form surfaces them as the primary inputs
    with display name as a secondary "How your name appears" field.

(2) Remove the broken Notifications card from user-settings (it called
    PATCH on an endpoint that has GET/PUT only and used a flat shape
    vs the actual array shape). Replace with the working
    NotificationPreferencesForm + ReminderDigestForm under a
    #notifications anchor. /notifications/preferences becomes a
    server-side redirect to /settings#notifications for back-compat;
    the mobile More-sheet + user-menu Bell entry now deep-link to the
    new anchor directly.

Drops the auto-generated drizzle-kit catch-up migration so we're not
sneaking accumulated schema drift into the journal — only the targeted
0049 lands here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 18:36:31 +02:00
parent 7c25d1aef6
commit 07b5756014
8 changed files with 124 additions and 96 deletions

View File

@@ -224,6 +224,17 @@ export const userProfiles = pgTable(
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').notNull().unique(), // references Better Auth user ID
/**
* Canonical first/last name pair. Added 2026-05-09 as the primary
* source for greetings, invoicing, and DocSign field-merging — the
* older `displayName` is now kept around as a derived/optional
* override (e.g. for nicknames or vanity formatting). When migrating
* production, backfill these columns from displayName by splitting
* on the first space and zero-pad the trailing column with NULL so
* single-token names don't fail the not-null assumption.
*/
firstName: text('first_name'),
lastName: text('last_name'),
displayName: text('display_name').notNull(),
avatarUrl: text('avatar_url'),
/** FK into the polymorphic `files` table — the avatar is stored