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:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "comment" TEXT;
|
||||
@@ -23,6 +23,7 @@ model User {
|
||||
banned Boolean? @default(false)
|
||||
banReason String?
|
||||
banExpires DateTime?
|
||||
comment String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user