feat(admin+search): user-mgmt polish, role labels, search keyword index

Admin search now matches against per-card keyword lists so typing
"client portal", "smtp", "tier ladder" lands on the System Settings card
(which hosts those flags). The same keyword list extends the topbar
global search (NAV_CATALOG) so any setting key resolves from the cmd-K
input — settings results sort to the bottom of the dropdown beneath
entity hits.

User management:
- Third action button (Power/PowerOff) enables/disables sign-in from the
  desktop list; mobile card dropdown gains the same item. Backed by the
  existing userProfiles.isActive flag — withAuth already refuses
  disabled sessions with 403.
- UserForm collects first + last name (canonical) alongside displayName,
  with admin email-change behind a confirmation modal. On confirm we
  send the OLD address an automated "your admin changed your sign-in
  email" notice (new template at admin-email-change.ts) and rewrite
  the Better Auth user row.
- Phone field swaps the bare tel input for the shared PhoneInput
  (country combobox + AsYouType formatting + E.164 storage).
- "Manage permissions" link points to /admin/roles?focusUser=… as
  a stepping stone for the future fine-tuned-permissions UI.

Role names normalize through a new ROLE_LABELS + formatRole() helper
in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the
prettifyRoleName in role-list; user-list and user-card now render
"Sales Agent" instead of "sales_agent". Custom roles pass through
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:14:12 +02:00
parent 0ab7055cf1
commit 660553c074
19 changed files with 1257 additions and 400 deletions

View File

@@ -5,6 +5,8 @@ export const createUserSchema = z.object({
name: z.string().min(1).max(200),
password: z.string().min(12),
displayName: z.string().min(1).max(200),
firstName: z.string().min(1).max(200).nullable().optional(),
lastName: z.string().min(1).max(200).nullable().optional(),
phone: z.string().optional(),
roleId: z.string().uuid(),
residentialAccess: z.boolean().optional().default(false),
@@ -14,6 +16,16 @@ export type CreateUserInput = z.infer<typeof createUserSchema>;
export const updateUserSchema = z.object({
displayName: z.string().min(1).max(200).optional(),
firstName: z.string().min(1).max(200).nullable().optional(),
lastName: z.string().min(1).max(200).nullable().optional(),
fullName: z.string().min(1).max(400).optional(),
/** Admin-initiated email change. When changed, the original address
* receives an automated heads-up email (see notifyEmailChange). */
email: z.string().email().optional(),
/** Set true alongside `email` to send the "your admin changed your
* sign-in email" notification to the prior address. UI sets this when
* the admin confirms the warning dialog. */
notifyEmailChange: z.boolean().optional(),
phone: z.string().nullable().optional(),
isActive: z.boolean().optional(),
roleId: z.string().uuid().optional(),