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(
|
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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user