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:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -64,6 +64,8 @@ export default async function UserPage({ params }: Props) {
|
||||
<div style={{ borderTop: "2px solid var(--border)", paddingTop: "1rem" }}>
|
||||
<UserContactEditor
|
||||
userId={userId}
|
||||
name={user.name ?? ""}
|
||||
email={user.email}
|
||||
phone={user.phone ?? null}
|
||||
birthday={user.birthday ?? null}
|
||||
/>
|
||||
|
||||
@@ -13,14 +13,23 @@ const inputStyle = {
|
||||
fontFamily: "inherit",
|
||||
} 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 {
|
||||
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 (
|
||||
<div className="space-y-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">
|
||||
<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)}
|
||||
placeholder="+7 900 000-00-00"
|
||||
style={inputStyle}
|
||||
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
||||
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
||||
{...focusHandlers}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<Set<string>>(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
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<label className="text-xs uppercase tracking-wider block mb-1" style={{ color: "var(--muted-foreground)" }}>
|
||||
Срок доступа
|
||||
Дней (0 — бессрочно)
|
||||
</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 className="pt-5">
|
||||
<button onClick={handleBulkGrant} disabled={pending} className="btn-aubade btn-aubade-accent">
|
||||
|
||||
Reference in New Issue
Block a user