admin/app/(dashboard)/users/[id]/_components/UserEditModal.tsx
alisaza 96603d31f7 feat(alert-modal): integrate alert modal for confirmation actions
Enhanced various components to utilize the alert modal for user confirmations before executing critical actions. This includes marking messages as read, saving notification rules, restoring reviews, sending replies, changing ticket statuses, and managing event commissions. The integration improves user experience by ensuring actions are intentional and provides clear feedback on the outcomes.
2026-09-13 10:14:18 +03:30

271 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { addToast } from '@/lib/toast'
import type { AdminUserDetail } from '@/services/adminUserDetail'
import Modal from '@/components/modals/Modal'
import Input from '@/components/formElements/Input'
import { AdminFormSection } from '@/components/forms/AdminFormLayout'
import UnsavedChangesIndicator from '@/components/forms/UnsavedChangesIndicator'
import { ADMIN_UPDATE_USER, type AdminUpdateUserPayload } from '@/services/adminUsers'
import { API_ROUTES } from '@/services/config'
import { AdminUserEditValidation, type AdminUserEditValues } from '@/validation/adminUsers'
import useAlertModal from '@/hooks/useAlertModal'
import axiosInstance from '@/config/axios'
const STATUS_OPTIONS = [
{ id: 'active', name: 'فعال' },
{ id: 'suspended', name: 'معلق' },
]
const HOST_PLAN_OPTIONS = [
{ id: 'free', name: 'رایگان (حداکثر ۱۰ ایونت)' },
{ id: 'unlimited', name: 'نامحدود' },
]
const GENDER_OPTIONS = [
{ id: 'male', name: 'مرد' },
{ id: 'female', name: 'زن' },
{ id: 'other', name: 'سایر' },
]
interface CityOption {
id: number
name: string
}
const toFormValues = (user: AdminUserDetail): AdminUserEditValues => ({
firstName: user.firstName ?? '',
lastName: user.lastName ?? '',
gender: user.gender ?? '',
cityId: user.cityId ? String(user.cityId) : '',
avatarUrl: user.avatarUrl ?? '',
bio: user.bio ?? '',
status: user.status === 'suspended' ? 'suspended' : 'active',
hostPlan: user.hostPlan ?? 'free',
})
interface UserEditModalProps {
isOpen: boolean
onOpenChange: (isOpen: boolean) => void
user: AdminUserDetail
/** Current admin's own id — status can't be self-edited. */
currentAdminId?: string
onSuccess: () => void
}
const UserEditModal = ({ isOpen, onOpenChange, user, currentAdminId, onSuccess }: UserEditModalProps) => {
const { showAlert } = useAlertModal()
const [submitting, setSubmitting] = useState(false)
const [cities, setCities] = useState<CityOption[]>([])
const isSelfEdit = Boolean(currentAdminId) && currentAdminId === user.id
const form = useForm<AdminUserEditValues>({
resolver: zodResolver(AdminUserEditValidation),
defaultValues: toFormValues(user),
})
const { reset } = form
// Modal stays mounted across opens, so reset explicitly whenever it opens
// (and the target user's data may have refreshed since the last edit).
useEffect(() => {
if (isOpen) {
reset(toFormValues(user))
}
}, [isOpen, reset, user])
useEffect(() => {
if (!isOpen || cities.length > 0) return
axiosInstance
.get(API_ROUTES.GEOGRAPHY.ALL_CITIES)
.then((res) => {
const payload: unknown = res.data
let list: unknown = []
if (Array.isArray(payload)) {
list = payload
} else if (typeof payload === 'object' && payload !== null) {
const record = payload as { data?: unknown; body?: unknown }
list = record.data ?? record.body ?? []
}
setCities(Array.isArray(list) ? (list as { id: number; name: string }[]) : [])
})
.catch(() => {
setCities([])
})
}, [isOpen, cities.length])
const buildDiffPayload = (values: AdminUserEditValues): AdminUpdateUserPayload => {
const initial = toFormValues(user)
const payload: AdminUpdateUserPayload = {}
if (values.firstName && values.firstName !== initial.firstName) payload.firstName = values.firstName
if (values.lastName && values.lastName !== initial.lastName) payload.lastName = values.lastName
if (values.gender && values.gender !== initial.gender) payload.gender = values.gender
if (values.cityId && values.cityId !== initial.cityId) payload.cityId = Number(values.cityId)
if (values.avatarUrl && values.avatarUrl !== initial.avatarUrl) payload.avatarUrl = values.avatarUrl
if (values.bio && values.bio !== initial.bio) payload.bio = values.bio
if (!isSelfEdit && values.status !== initial.status) payload.status = values.status
if (values.hostPlan !== initial.hostPlan) payload.hostPlan = values.hostPlan
return payload
}
const submitUpdate = async (values: AdminUserEditValues) => {
const payload = buildDiffPayload(values)
if (Object.keys(payload).length === 0) {
onOpenChange(false)
return
}
setSubmitting(true)
const result = await ADMIN_UPDATE_USER(user.id, payload)
setSubmitting(false)
if (!result.ok) return
addToast({ title: 'اطلاعات کاربر ذخیره شد', color: 'success' })
onOpenChange(false)
onSuccess()
}
const handleSubmit = (values: AdminUserEditValues) => {
const initial = toFormValues(user)
const payload = buildDiffPayload(values)
if (Object.keys(payload).length === 0) {
onOpenChange(false)
return
}
if (!isSelfEdit && values.status === 'suspended' && initial.status !== 'suspended') {
showAlert('این کاربر معلق شود؟ کاربر تا فعال‌سازی مجدد امکان استفاده از حساب را نخواهد داشت.', () => submitUpdate(values), undefined, {
dangerAccept: true,
})
return
}
showAlert('تغییرات این کاربر ذخیره شود؟', () => submitUpdate(values))
}
return (
<Modal
acceptBtnText="ذخیره تغییرات"
isLoading={submitting}
isOpen={isOpen}
title={`ویرایش «${[user.firstName, user.lastName].filter(Boolean).join(' ') || user.mobile}»`}
onAccept={form.handleSubmit(handleSubmit)}
onOpenChange={onOpenChange}
>
<FormProvider {...form}>
<form
className="flex flex-col gap-4"
onSubmit={form.handleSubmit(handleSubmit)}
>
<UnsavedChangesIndicator isDirty={form.formState.isDirty} />
<AdminFormSection
contained={false}
description="اطلاعات عمومی نمایش‌داده‌شده در پروفایل کاربر"
title="اطلاعات پروفایل"
>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Input
generalType="input"
label="نام"
name="firstName"
placeholder="نام"
variant="flat"
/>
<Input
generalType="input"
label="نام خانوادگی"
name="lastName"
placeholder="نام خانوادگی"
variant="flat"
/>
</div>
<Input
generalType="select"
label="پلن میزبانی"
name="hostPlan"
selectKey="id"
selectOptions={HOST_PLAN_OPTIONS}
selectValue="name"
/>
<Input
generalType="select"
label="جنسیت"
name="gender"
selectKey="id"
selectOptions={GENDER_OPTIONS}
selectValue="name"
/>
<Input
generalType="select"
label="شهر"
name="cityId"
selectKey="id"
selectOptions={cities}
selectValue="name"
/>
<Input
direction="ltr"
generalType="input"
label="آدرس تصویر پروفایل"
name="avatarUrl"
placeholder="https://..."
variant="flat"
/>
<Input
generalType="textarea"
label="بیوگرافی"
name="bio"
placeholder="بیوگرافی کوتاه کاربر"
variant="flat"
/>
</div>
</AdminFormSection>
<AdminFormSection
contained={false}
description="سطح دسترسی و امکان استفاده از حساب"
title="تنظیمات مدیریتی"
>
<div className="flex flex-col gap-4">
{isSelfEdit && (
<div className="rounded-xl border border-fourth-100 bg-fourth-100 px-3 py-2 text-xs leading-5 text-fourth-900">
وضعیت حساب خودتان از اینجا قابل ویرایش نیست.
</div>
)}
<Input
disabled={isSelfEdit}
generalType="select"
label="وضعیت حساب"
name="status"
selectKey="id"
selectOptions={STATUS_OPTIONS}
selectValue="name"
/>
</div>
</AdminFormSection>
</form>
</FormProvider>
</Modal>
)
}
export default UserEditModal