Add name/email editing and days-based course access in admin user card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 12:01:01 +05:00
parent e691124058
commit 6b5bfc853e
4 changed files with 69 additions and 15 deletions
+3 -1
View File
@@ -43,12 +43,14 @@ export async function bulkGrantAccess(
export async function updateUserContact( export async function updateUserContact(
userId: string, userId: string,
data: { phone: string; birthday: string } data: { name: string; email: string; phone: string; birthday: string }
) { ) {
await requireAdmin(); await requireAdmin();
await prisma.user.update({ await prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { data: {
name: data.name.trim() || undefined,
email: data.email.trim() || undefined,
phone: data.phone.trim() || null, phone: data.phone.trim() || null,
birthday: data.birthday ? new Date(data.birthday) : null, birthday: data.birthday ? new Date(data.birthday) : null,
}, },
+2
View File
@@ -64,6 +64,8 @@ export default async function UserPage({ params }: Props) {
<div style={{ borderTop: "2px solid var(--border)", paddingTop: "1rem" }}> <div style={{ borderTop: "2px solid var(--border)", paddingTop: "1rem" }}>
<UserContactEditor <UserContactEditor
userId={userId} userId={userId}
name={user.name ?? ""}
email={user.email}
phone={user.phone ?? null} phone={user.phone ?? null}
birthday={user.birthday ?? null} birthday={user.birthday ?? null}
/> />
+37 -6
View File
@@ -13,14 +13,23 @@ const inputStyle = {
fontFamily: "inherit", fontFamily: "inherit",
} as React.CSSProperties; } as React.CSSProperties;
const focusHandlers = {
onFocus: (e: React.FocusEvent<HTMLInputElement>) => (e.currentTarget.style.borderColor = "var(--foreground)"),
onBlur: (e: React.FocusEvent<HTMLInputElement>) => (e.currentTarget.style.borderColor = "var(--border)"),
};
interface Props { interface Props {
userId: string; userId: string;
name: string;
email: string;
phone: string | null; phone: string | null;
birthday: Date | 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 [editing, setEditing] = useState(false);
const [nameVal, setNameVal] = useState(name);
const [emailVal, setEmailVal] = useState(email);
const [phoneVal, setPhoneVal] = useState(phone ?? ""); const [phoneVal, setPhoneVal] = useState(phone ?? "");
const [birthdayVal, setBirthdayVal] = useState( const [birthdayVal, setBirthdayVal] = useState(
birthday ? birthday.toISOString().slice(0, 10) : "" birthday ? birthday.toISOString().slice(0, 10) : ""
@@ -29,7 +38,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
function handleSave() { function handleSave() {
startTransition(async () => { startTransition(async () => {
await updateUserContact(userId, { phone: phoneVal, birthday: birthdayVal }); await updateUserContact(userId, { name: nameVal, email: emailVal, phone: phoneVal, birthday: birthdayVal });
setEditing(false); setEditing(false);
}); });
} }
@@ -68,6 +77,30 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Имя
</label>
<input
type="text"
value={nameVal}
onChange={(e) => setNameVal(e.target.value)}
style={inputStyle}
{...focusHandlers}
/>
</div>
<div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Email
</label>
<input
type="email"
value={emailVal}
onChange={(e) => setEmailVal(e.target.value)}
style={inputStyle}
{...focusHandlers}
/>
</div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}> <label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Телефон Телефон
@@ -78,8 +111,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
onChange={(e) => setPhoneVal(e.target.value)} onChange={(e) => setPhoneVal(e.target.value)}
placeholder="+7 900 000-00-00" placeholder="+7 900 000-00-00"
style={inputStyle} style={inputStyle}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} {...focusHandlers}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -91,8 +123,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
value={birthdayVal} value={birthdayVal}
onChange={(e) => setBirthdayVal(e.target.value)} onChange={(e) => setBirthdayVal(e.target.value)}
style={inputStyle} style={inputStyle}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} {...focusHandlers}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
/> />
</div> </div>
</div> </div>
@@ -1,8 +1,6 @@
"use client"; "use client";
import { useState, useTransition } from "react"; 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"; import { bulkGrantAccess, revokeUserAccess } from "@/lib/actions/user-actions";
interface Course { interface Course {
@@ -28,7 +26,7 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
() => new Map(enrollments.map((e) => [e.courseId, e.expiresAt])) () => new Map(enrollments.map((e) => [e.courseId, e.expiresAt]))
); );
const [selected, setSelected] = useState<Set<string>>(new Set()); const [selected, setSelected] = useState<Set<string>>(new Set());
const [expiryDate, setExpiryDate] = useState(""); const [days, setDays] = useState("");
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
const unenrolled = allCourses.filter((c) => !enrolledMap.has(c.id)); const unenrolled = allCourses.filter((c) => !enrolledMap.has(c.id));
@@ -44,12 +42,16 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
function handleBulkGrant() { function handleBulkGrant() {
if (selected.size === 0) return; if (selected.size === 0) return;
const ids = [...selected]; 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); 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); setEnrolledMap(newMap);
setSelected(new Set()); setSelected(new Set());
startTransition(() => bulkGrantAccess(userId, ids, expiry)); setDays("");
startTransition(() => bulkGrantAccess(userId, ids, expiresAt));
} }
function handleRevoke(courseId: string) { function handleRevoke(courseId: string) {
@@ -119,9 +121,26 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div> <div>
<label className="text-xs uppercase tracking-wider block mb-1" style={{ color: "var(--muted-foreground)" }}> <label className="text-xs uppercase tracking-wider block mb-1" style={{ color: "var(--muted-foreground)" }}>
Срок доступа Дней (0 бессрочно)
</label> </label>
<Input type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)} className="w-44" /> <input
type="number"
min="0"
value={days}
onChange={(e) => 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)")}
/>
</div> </div>
<div className="pt-5"> <div className="pt-5">
<button onClick={handleBulkGrant} disabled={pending} className="btn-aubade btn-aubade-accent"> <button onClick={handleBulkGrant} disabled={pending} className="btn-aubade btn-aubade-accent">