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:
@@ -59,10 +59,24 @@ function timeOfDayGreeting(): string {
|
||||
return 'Good evening';
|
||||
}
|
||||
|
||||
export function DashboardShell() {
|
||||
interface DashboardShellProps {
|
||||
/** SSR-prefetched first name. When provided, the greeting renders with it
|
||||
* on first paint instead of flickering "Welcome back" → "Hello, Matt". */
|
||||
initialFirstName?: string | null;
|
||||
/** SSR-prefetched widget visibility map. Seeds the preferences cache so the
|
||||
* layout doesn't reflow once the client-side fetch resolves. */
|
||||
initialWidgetVisibility?: Record<string, boolean> | null;
|
||||
}
|
||||
|
||||
export function DashboardShell({
|
||||
initialFirstName,
|
||||
initialWidgetVisibility,
|
||||
}: DashboardShellProps = {}) {
|
||||
const [range, setRange] = useState<DateRange>('30d');
|
||||
|
||||
const { visibleWidgets } = useDashboardWidgets();
|
||||
const { visibleWidgets } = useDashboardWidgets({
|
||||
initialVisibility: initialWidgetVisibility ?? null,
|
||||
});
|
||||
|
||||
// Bucket once so the JSX stays readable. Registry order is preserved
|
||||
// inside each bucket, so reordering the registry reorders the render.
|
||||
@@ -72,12 +86,17 @@ export function DashboardShell() {
|
||||
|
||||
// Reuses the existing ['me'] cache (5-minute staleTime) populated by
|
||||
// useTablePreferences elsewhere — usually a cache hit, so no extra
|
||||
// request. Falls back to a generic greeting if the profile isn't
|
||||
// available yet so we never block the dashboard render.
|
||||
// request. When the page server-prefetches the first name we seed it
|
||||
// here via `initialData` so the cache is warm before the post-mount
|
||||
// fetch resolves, eliminating the "Welcome back → Hello, Matt" flash.
|
||||
const me = useQuery<MeData>({
|
||||
queryKey: ['me'],
|
||||
queryFn: ({ signal }) => apiFetch<MeData>('/api/v1/me', { signal }),
|
||||
staleTime: 5 * 60_000,
|
||||
initialData:
|
||||
initialFirstName !== undefined
|
||||
? ({ data: { profile: { firstName: initialFirstName } } } as MeData)
|
||||
: undefined,
|
||||
});
|
||||
const firstName = me.data?.data?.profile?.firstName?.trim();
|
||||
|
||||
@@ -91,10 +110,7 @@ export function DashboardShell() {
|
||||
setClientGreeting(timeOfDayGreeting());
|
||||
// Re-evaluate hourly so a rep who leaves the dashboard open through a
|
||||
// boundary (5am, noon, 6pm) doesn't keep stale text on screen.
|
||||
const interval = window.setInterval(
|
||||
() => setClientGreeting(timeOfDayGreeting()),
|
||||
60 * 60_000,
|
||||
);
|
||||
const interval = window.setInterval(() => setClientGreeting(timeOfDayGreeting()), 60 * 60_000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -60,8 +60,7 @@ export function TimezoneDriftBanner() {
|
||||
}
|
||||
void apiFetch<MeResponse>('/api/v1/me')
|
||||
.then((res) => {
|
||||
const tz =
|
||||
res.data.profile?.preferences?.timezone ?? res.data.profile?.timezone ?? null;
|
||||
const tz = res.data.profile?.preferences?.timezone ?? res.data.profile?.timezone ?? null;
|
||||
setStored(tz);
|
||||
})
|
||||
.catch(() => setStored(null))
|
||||
|
||||
Reference in New Issue
Block a user