diff --git a/apps/web/src/components/admin/UsersClient.tsx b/apps/web/src/components/admin/UsersClient.tsx index fc21dba..6126605 100644 --- a/apps/web/src/components/admin/UsersClient.tsx +++ b/apps/web/src/components/admin/UsersClient.tsx @@ -92,6 +92,7 @@ export function UsersClient() { const [actionError, setActionError] = useState(null); const [search, setSearch] = useState(""); const [roleFilter, setRoleFilter] = useState(""); + const [editingName, setEditingName] = useState<{ userId: string; name: string } | null>(null); const [passwordTarget, setPasswordTarget] = useState<{ userId: string; userName: string } | null>(null); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -187,6 +188,14 @@ export function UsersClient() { onError: (err) => setPasswordError(err.message), }); + const updateNameMutation = trpc.user.updateName.useMutation({ + onSuccess: async () => { + await utils.user.list.invalidate(); + setEditingName(null); + }, + onError: (err) => setActionError(err.message), + }); + function openSetPassword(user: UserRow) { setPasswordTarget({ userId: user.id, userName: user.name ?? user.email }); setNewPassword(""); @@ -756,6 +765,61 @@ export function UsersClient() { {/* Modal Body */}
+ {/* User Name */} +
+

+ Display Name +

+ {editingName?.userId === editState.userId ? ( +
+ setEditingName({ ...editingName, name: e.target.value })} + className="flex-1 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter" && editingName.name.trim()) { + updateNameMutation.mutate({ id: editingName.userId, name: editingName.name.trim() }); + } + if (e.key === "Escape") setEditingName(null); + }} + /> + + +
+ ) : ( +
+ + {(users as any)?.find((u: any) => u.id === editState.userId)?.name ?? "—"} + + +
+ )} +
+ {/* System Role */}

diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 57e5091..f5f20cb 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -196,6 +196,41 @@ export const userRouter = createTRPCRouter({ return updated; }), + updateName: adminProcedure + .input( + z.object({ + id: z.string(), + name: z.string().min(1, "Name is required").max(200), + }), + ) + .mutation(async ({ ctx, input }) => { + const before = await ctx.db.user.findUniqueOrThrow({ + where: { id: input.id }, + select: { id: true, name: true, email: true }, + }); + + const updated = await ctx.db.user.update({ + where: { id: input.id }, + data: { name: input.name }, + select: { id: true, name: true, email: true }, + }); + + void createAuditEntry({ + db: ctx.db, + entityType: "User", + entityId: updated.id, + entityName: `${updated.name} (${updated.email})`, + action: "UPDATE", + ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}), + before: before as unknown as Record, + after: updated as unknown as Record, + source: "ui", + summary: `Changed name from "${before.name}" to "${updated.name}"`, + }); + + return updated; + }), + // ─── Resource Linking ────────────────────────────────────────────────── linkResource: adminProcedure