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:
@@ -26,7 +26,12 @@ import {
|
||||
import { buildListQuery } from '@/lib/db/query-builder';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { softDelete, restore, withTransaction } from '@/lib/db/utils';
|
||||
import { PIPELINE_STAGES, canTransitionStage, type PipelineStage } from '@/lib/constants';
|
||||
import {
|
||||
PIPELINE_STAGES,
|
||||
STAGE_LABELS,
|
||||
canTransitionStage,
|
||||
type PipelineStage,
|
||||
} from '@/lib/constants';
|
||||
import type {
|
||||
CreateInterestInput,
|
||||
UpdateInterestInput,
|
||||
@@ -824,21 +829,42 @@ export async function changeInterestStage(
|
||||
}),
|
||||
);
|
||||
|
||||
// Fire-and-forget notification to the acting user
|
||||
void import('@/lib/services/notifications.service').then(({ createNotification }) =>
|
||||
createNotification({
|
||||
// Fire-and-forget notification to the acting user. Resolve a friendly
|
||||
// label (client full name → primary mooring number → "this interest") so
|
||||
// the inbox doesn't surface a raw UUID; stage names go through the
|
||||
// canonical STAGE_LABELS dictionary so "deposit_10pct" reads as
|
||||
// "10% Deposit" everywhere.
|
||||
void (async () => {
|
||||
const [{ createNotification }, clientRow, primaryBerth] = await Promise.all([
|
||||
import('@/lib/services/notifications.service'),
|
||||
db.query.clients.findFirst({
|
||||
where: eq(clients.id, existing.clientId),
|
||||
columns: { fullName: true },
|
||||
}),
|
||||
getPrimaryBerth(id).catch(() => null),
|
||||
]);
|
||||
const subject =
|
||||
clientRow?.fullName ??
|
||||
(primaryBerth ? `Berth ${primaryBerth.mooringNumber}` : 'this interest');
|
||||
const fromLabel = oldStage
|
||||
? (STAGE_LABELS[oldStage as PipelineStage] ?? oldStage.replace(/_/g, ' '))
|
||||
: 'unknown';
|
||||
const toLabel =
|
||||
STAGE_LABELS[data.pipelineStage as PipelineStage] ??
|
||||
data.pipelineStage.replace(/_/g, ' ');
|
||||
await createNotification({
|
||||
portId,
|
||||
userId: meta.userId,
|
||||
type: 'interest_stage_changed',
|
||||
title: `Interest moved to ${data.pipelineStage}`,
|
||||
description: `Interest ${id} stage changed from ${oldStage ?? 'unknown'} to ${data.pipelineStage}`,
|
||||
title: `${subject} moved to ${toLabel}`,
|
||||
description: `Stage changed from ${fromLabel} to ${toLabel}.`,
|
||||
link: `/interests/${id}`,
|
||||
entityType: 'interest',
|
||||
entityId: id,
|
||||
dedupeKey: `interest:${id}:stage:${data.pipelineStage}`,
|
||||
cooldownMs: 300_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
})();
|
||||
|
||||
return updated!;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user