diff --git a/prisma/migrations/20260427100000_add_phone_birthday/migration.sql b/prisma/migrations/20260427100000_add_phone_birthday/migration.sql new file mode 100644 index 0000000..f654216 --- /dev/null +++ b/prisma/migrations/20260427100000_add_phone_birthday/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "phone" TEXT; +ALTER TABLE "User" ADD COLUMN "birthday" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 022c129..957f854 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,8 @@ model User { emailVerified Boolean @default(false) image String? role String @default("student") // student | curator | admin + phone String? + birthday DateTime? banned Boolean? @default(false) banReason String? banExpires DateTime? diff --git a/src/app/admin/users/[userId]/actions.ts b/src/app/admin/users/[userId]/actions.ts index b96a0fa..2aaf2a2 100644 --- a/src/app/admin/users/[userId]/actions.ts +++ b/src/app/admin/users/[userId]/actions.ts @@ -41,6 +41,21 @@ export async function bulkGrantAccess( revalidatePath(`/admin/users/${userId}`); } +export async function updateUserContact( + userId: string, + data: { phone: string; birthday: string } +) { + await requireAdmin(); + await prisma.user.update({ + where: { id: userId }, + data: { + phone: data.phone.trim() || null, + birthday: data.birthday ? new Date(data.birthday) : null, + }, + }); + revalidatePath(`/admin/users/${userId}`); +} + export async function revokeUserAccess(userId: string, courseId: string) { const session = await requireAdmin(); await prisma.courseEnrollment.delete({ diff --git a/src/app/admin/users/[userId]/page.tsx b/src/app/admin/users/[userId]/page.tsx index 06b7062..300b87b 100644 --- a/src/app/admin/users/[userId]/page.tsx +++ b/src/app/admin/users/[userId]/page.tsx @@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma"; import { notFound } from "next/navigation"; import Link from "next/link"; import { UserEnrollmentManager } from "@/components/admin/user-enrollment-manager"; +import { UserContactEditor } from "@/components/admin/user-contact-editor"; interface Props { params: Promise<{ userId: string }>; @@ -47,7 +48,7 @@ export default async function UserPage({ params }: Props) { {/* User info */} -
+

{user.name}

@@ -60,6 +61,13 @@ export default async function UserPage({ params }: Props) {
+
+ +
{/* Enrollments + bulk grant */} diff --git a/src/components/admin/user-contact-editor.tsx b/src/components/admin/user-contact-editor.tsx new file mode 100644 index 0000000..e6f44e1 --- /dev/null +++ b/src/components/admin/user-contact-editor.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { updateUserContact } from "@/app/admin/users/[userId]/actions"; + +const inputStyle = { + border: "2px solid var(--border)", + background: "var(--background)", + outline: "none", + width: "100%", + padding: "0.4rem 0.6rem", + fontSize: "0.875rem", + fontFamily: "inherit", +} as React.CSSProperties; + +interface Props { + userId: string; + phone: string | null; + birthday: Date | null; +} + +export function UserContactEditor({ userId, phone, birthday }: Props) { + const [editing, setEditing] = useState(false); + const [phoneVal, setPhoneVal] = useState(phone ?? ""); + const [birthdayVal, setBirthdayVal] = useState( + birthday ? birthday.toISOString().slice(0, 10) : "" + ); + const [pending, startTransition] = useTransition(); + + function handleSave() { + startTransition(async () => { + await updateUserContact(userId, { phone: phoneVal, birthday: birthdayVal }); + setEditing(false); + }); + } + + if (!editing) { + return ( +
+
+

+ Телефон +

+

{phone || "—"}

+
+
+

+ День рождения +

+

+ {birthday + ? new Date(birthday).toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" }) + : "—"} +

+
+ +
+ ); + } + + return ( +
+
+
+ + 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)")} + /> +
+
+ + setBirthdayVal(e.target.value)} + style={inputStyle} + onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")} + onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} + /> +
+
+
+ + +
+
+ ); +}