From 6b5bfc853efb483537dc15ee3e0cc24441654a94 Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Tue, 28 Apr 2026 12:01:01 +0500 Subject: [PATCH] Add name/email editing and days-based course access in admin user card Co-Authored-By: Claude Sonnet 4.6 --- src/app/admin/users/[userId]/actions.ts | 4 +- src/app/admin/users/[userId]/page.tsx | 2 + src/components/admin/user-contact-editor.tsx | 43 ++++++++++++++++--- .../admin/user-enrollment-manager.tsx | 35 +++++++++++---- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/app/admin/users/[userId]/actions.ts b/src/app/admin/users/[userId]/actions.ts index 2aaf2a2..09f0f92 100644 --- a/src/app/admin/users/[userId]/actions.ts +++ b/src/app/admin/users/[userId]/actions.ts @@ -43,12 +43,14 @@ export async function bulkGrantAccess( export async function updateUserContact( userId: string, - data: { phone: string; birthday: string } + data: { name: string; email: string; phone: string; birthday: string } ) { await requireAdmin(); await prisma.user.update({ where: { id: userId }, data: { + name: data.name.trim() || undefined, + email: data.email.trim() || undefined, phone: data.phone.trim() || null, birthday: data.birthday ? new Date(data.birthday) : null, }, diff --git a/src/app/admin/users/[userId]/page.tsx b/src/app/admin/users/[userId]/page.tsx index 300b87b..8ec3ede 100644 --- a/src/app/admin/users/[userId]/page.tsx +++ b/src/app/admin/users/[userId]/page.tsx @@ -64,6 +64,8 @@ export default async function UserPage({ params }: Props) {
diff --git a/src/components/admin/user-contact-editor.tsx b/src/components/admin/user-contact-editor.tsx index e6f44e1..8764622 100644 --- a/src/components/admin/user-contact-editor.tsx +++ b/src/components/admin/user-contact-editor.tsx @@ -13,14 +13,23 @@ const inputStyle = { fontFamily: "inherit", } as React.CSSProperties; +const focusHandlers = { + onFocus: (e: React.FocusEvent) => (e.currentTarget.style.borderColor = "var(--foreground)"), + onBlur: (e: React.FocusEvent) => (e.currentTarget.style.borderColor = "var(--border)"), +}; + interface Props { userId: string; + name: string; + email: string; phone: string | null; birthday: Date | null; } -export function UserContactEditor({ userId, phone, birthday }: Props) { +export function UserContactEditor({ userId, name, email, phone, birthday }: Props) { const [editing, setEditing] = useState(false); + const [nameVal, setNameVal] = useState(name); + const [emailVal, setEmailVal] = useState(email); const [phoneVal, setPhoneVal] = useState(phone ?? ""); const [birthdayVal, setBirthdayVal] = useState( birthday ? birthday.toISOString().slice(0, 10) : "" @@ -29,7 +38,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) { function handleSave() { startTransition(async () => { - await updateUserContact(userId, { phone: phoneVal, birthday: birthdayVal }); + await updateUserContact(userId, { name: nameVal, email: emailVal, phone: phoneVal, birthday: birthdayVal }); setEditing(false); }); } @@ -68,6 +77,30 @@ export function UserContactEditor({ userId, phone, birthday }: Props) { return (
+
+ + setNameVal(e.target.value)} + style={inputStyle} + {...focusHandlers} + /> +
+
+ + setEmailVal(e.target.value)} + style={inputStyle} + {...focusHandlers} + /> +
@@ -91,8 +123,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) { value={birthdayVal} onChange={(e) => setBirthdayVal(e.target.value)} style={inputStyle} - onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} - onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} + {...focusHandlers} />
diff --git a/src/components/admin/user-enrollment-manager.tsx b/src/components/admin/user-enrollment-manager.tsx index ed5edd0..eb87398 100644 --- a/src/components/admin/user-enrollment-manager.tsx +++ b/src/components/admin/user-enrollment-manager.tsx @@ -1,8 +1,6 @@ "use client"; import { useState, useTransition } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { bulkGrantAccess, revokeUserAccess } from "@/lib/actions/user-actions"; interface Course { @@ -28,7 +26,7 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props () => new Map(enrollments.map((e) => [e.courseId, e.expiresAt])) ); const [selected, setSelected] = useState>(new Set()); - const [expiryDate, setExpiryDate] = useState(""); + const [days, setDays] = useState(""); const [pending, startTransition] = useTransition(); const unenrolled = allCourses.filter((c) => !enrolledMap.has(c.id)); @@ -44,12 +42,16 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props function handleBulkGrant() { if (selected.size === 0) return; const ids = [...selected]; - const expiry = expiryDate || null; + const daysNum = parseInt(days, 10); + const expiresAt = !isNaN(daysNum) && daysNum > 0 + ? new Date(Date.now() + daysNum * 86_400_000).toISOString() + : null; const newMap = new Map(enrolledMap); - ids.forEach((id) => newMap.set(id, expiry ? new Date(expiry) : null)); + ids.forEach((id) => newMap.set(id, expiresAt ? new Date(expiresAt) : null)); setEnrolledMap(newMap); setSelected(new Set()); - startTransition(() => bulkGrantAccess(userId, ids, expiry)); + setDays(""); + startTransition(() => bulkGrantAccess(userId, ids, expiresAt)); } function handleRevoke(courseId: string) { @@ -119,9 +121,26 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
- setExpiryDate(e.target.value)} className="w-44" /> + setDays(e.target.value)} + placeholder="0" + style={{ + border: "2px solid var(--border)", + background: "var(--background)", + outline: "none", + padding: "0.4rem 0.6rem", + fontSize: "0.875rem", + fontFamily: "inherit", + width: "6rem", + }} + onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} + onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} + />