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.
271 lines
8.9 KiB
TypeScript
271 lines
8.9 KiB
TypeScript
'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
|