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)
|
banned Boolean? @default(false)
|
||||||
banReason String?
|
banReason String?
|
||||||
banExpires DateTime?
|
banExpires DateTime?
|
||||||
|
comment String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function bulkGrantAccess(
|
|||||||
|
|
||||||
export async function updateUserContact(
|
export async function updateUserContact(
|
||||||
userId: string,
|
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 requireAdmin();
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
@@ -53,6 +53,7 @@ export async function updateUserContact(
|
|||||||
email: data.email.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,
|
||||||
|
comment: data.comment.trim() || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
revalidatePath(`/admin/users/${userId}`);
|
revalidatePath(`/admin/users/${userId}`);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default async function UserPage({ params }: Props) {
|
|||||||
email={user.email}
|
email={user.email}
|
||||||
phone={user.phone ?? null}
|
phone={user.phone ?? null}
|
||||||
birthday={user.birthday ?? null}
|
birthday={user.birthday ?? null}
|
||||||
|
comment={user.comment ?? null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ const inputStyle = {
|
|||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const focusHandlers = {
|
const focusHandlers = {
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) => (e.currentTarget.style.borderColor = "var(--foreground)"),
|
onFocus: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
||||||
onBlur: (e: React.FocusEvent<HTMLInputElement>) => (e.currentTarget.style.borderColor = "var(--border)"),
|
(e.currentTarget.style.borderColor = "var(--foreground)"),
|
||||||
|
onBlur: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
||||||
|
(e.currentTarget.style.borderColor = "var(--border)"),
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,9 +26,10 @@ interface Props {
|
|||||||
email: string;
|
email: string;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
birthday: Date | 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 [editing, setEditing] = useState(false);
|
||||||
const [nameVal, setNameVal] = useState(name);
|
const [nameVal, setNameVal] = useState(name);
|
||||||
const [emailVal, setEmailVal] = useState(email);
|
const [emailVal, setEmailVal] = useState(email);
|
||||||
@@ -34,17 +37,25 @@ export function UserContactEditor({ userId, name, email, phone, birthday }: Prop
|
|||||||
const [birthdayVal, setBirthdayVal] = useState(
|
const [birthdayVal, setBirthdayVal] = useState(
|
||||||
birthday ? birthday.toISOString().slice(0, 10) : ""
|
birthday ? birthday.toISOString().slice(0, 10) : ""
|
||||||
);
|
);
|
||||||
|
const [commentVal, setCommentVal] = useState(comment ?? "");
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
startTransition(async () => {
|
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);
|
setEditing(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
<div className="flex items-start gap-6 flex-wrap">
|
<div className="flex items-start gap-6 flex-wrap">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
<p className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
||||||
@@ -71,6 +82,15 @@ export function UserContactEditor({ userId, name, email, phone, birthday }: Prop
|
|||||||
Изменить
|
Изменить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{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>
|
</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">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user