Add comment field to user profile in admin panel

- Prisma: User.comment String? column + migration
- UserContactEditor: comment shown in view mode, textarea in edit mode
- updateUserContact action: saves comment to DB
This commit is contained in:
2026-05-06 14:06:53 +00:00
parent 4f3b389f05
commit 48721759d3
5 changed files with 68 additions and 29 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "comment" TEXT;
+1
View File
@@ -23,6 +23,7 @@ model User {
banned Boolean? @default(false)
banReason String?
banExpires DateTime?
comment String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+2 -1
View File
@@ -43,7 +43,7 @@ export async function bulkGrantAccess(
export async function updateUserContact(
userId: string,
data: { name: string; email: string; phone: string; birthday: string }
data: { name: string; email: string; phone: string; birthday: string; comment: string }
) {
await requireAdmin();
await prisma.user.update({
@@ -53,6 +53,7 @@ export async function updateUserContact(
email: data.email.trim() || undefined,
phone: data.phone.trim() || null,
birthday: data.birthday ? new Date(data.birthday) : null,
comment: data.comment.trim() || null,
},
});
revalidatePath(`/admin/users/${userId}`);
+1
View File
@@ -68,6 +68,7 @@ export default async function UserPage({ params }: Props) {
email={user.email}
phone={user.phone ?? null}
birthday={user.birthday ?? null}
comment={user.comment ?? null}
/>
</div>
</section>
+62 -28
View File
@@ -14,8 +14,10 @@ const inputStyle = {
} 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)"),
onFocus: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) =>
(e.currentTarget.style.borderColor = "var(--foreground)"),
onBlur: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) =>
(e.currentTarget.style.borderColor = "var(--border)"),
};
interface Props {
@@ -24,9 +26,10 @@ interface Props {
email: string;
phone: string | null;
birthday: Date | null;
comment: string | null;
}
export function UserContactEditor({ userId, name, email, phone, birthday }: Props) {
export function UserContactEditor({ userId, name, email, phone, birthday, comment }: Props) {
const [editing, setEditing] = useState(false);
const [nameVal, setNameVal] = useState(name);
const [emailVal, setEmailVal] = useState(email);
@@ -34,42 +37,59 @@ export function UserContactEditor({ userId, name, email, phone, birthday }: Prop
const [birthdayVal, setBirthdayVal] = useState(
birthday ? birthday.toISOString().slice(0, 10) : ""
);
const [commentVal, setCommentVal] = useState(comment ?? "");
const [pending, startTransition] = useTransition();
function handleSave() {
startTransition(async () => {
await updateUserContact(userId, { name: nameVal, email: emailVal, phone: phoneVal, birthday: birthdayVal });
await updateUserContact(userId, {
name: nameVal,
email: emailVal,
phone: phoneVal,
birthday: birthdayVal,
comment: commentVal,
});
setEditing(false);
});
}
if (!editing) {
return (
<div className="flex items-start gap-6 flex-wrap">
<div className="space-y-0.5">
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Телефон
</p>
<p className="text-sm">{phone || "—"}</p>
<div className="space-y-3">
<div className="flex items-start gap-6 flex-wrap">
<div className="space-y-0.5">
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Телефон
</p>
<p className="text-sm">{phone || "—"}</p>
</div>
<div className="space-y-0.5">
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
День рождения
</p>
<p className="text-sm">
{birthday
? new Date(birthday).toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" })
: "—"}
</p>
</div>
<button
type="button"
onClick={() => setEditing(true)}
className="text-xs underline self-end pb-0.5"
style={{ color: "var(--muted-foreground)" }}
>
Изменить
</button>
</div>
<div className="space-y-0.5">
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
День рождения
</p>
<p className="text-sm">
{birthday
? new Date(birthday).toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" })
: "—"}
</p>
</div>
<button
type="button"
onClick={() => setEditing(true)}
className="text-xs underline self-end pb-0.5"
style={{ color: "var(--muted-foreground)" }}
>
Изменить
</button>
{comment && (
<div className="space-y-0.5">
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Комментарий
</p>
<p className="text-sm whitespace-pre-wrap" style={{ color: "var(--foreground)" }}>{comment}</p>
</div>
)}
</div>
);
}
@@ -127,6 +147,20 @@ export function UserContactEditor({ userId, name, email, phone, birthday }: Prop
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Комментарий
</label>
<textarea
value={commentVal}
onChange={(e) => setCommentVal(e.target.value)}
rows={3}
placeholder="Заметки об этом пользователе..."
style={{ ...inputStyle, resize: "vertical" }}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
/>
</div>
<div className="flex gap-2">
<button
type="button"