|
|
@@ -0,0 +1,339 @@
|
|
|
+import { useEffect, useMemo } from 'react'
|
|
|
+import { z } from 'zod'
|
|
|
+import { useForm } from 'react-hook-form'
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
+import { toast } from 'sonner'
|
|
|
+import { getApiErrorMessage } from '@/lib/api-error'
|
|
|
+import { PERMISSIONS } from '@/constants/permissions'
|
|
|
+import { useAdmin } from '@/hooks/admins/useAdmin'
|
|
|
+import { useCreateAdmin } from '@/hooks/admins/useCreateAdmin'
|
|
|
+import { useUpdateAdmin } from '@/hooks/admins/useUpdateAdmin'
|
|
|
+import { useRoles } from '@/hooks/roles/useRoles'
|
|
|
+import { useCan } from '@/hooks/use-can'
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
+import { Checkbox } from '@/components/ui/checkbox'
|
|
|
+import {
|
|
|
+ Dialog,
|
|
|
+ DialogContent,
|
|
|
+ DialogFooter,
|
|
|
+ DialogHeader,
|
|
|
+ DialogTitle,
|
|
|
+} from '@/components/ui/dialog'
|
|
|
+import {
|
|
|
+ Form,
|
|
|
+ FormControl,
|
|
|
+ FormField,
|
|
|
+ FormItem,
|
|
|
+ FormLabel,
|
|
|
+ FormMessage,
|
|
|
+} from '@/components/ui/form'
|
|
|
+import { Input } from '@/components/ui/input'
|
|
|
+import {
|
|
|
+ Select,
|
|
|
+ SelectContent,
|
|
|
+ SelectItem,
|
|
|
+ SelectTrigger,
|
|
|
+ SelectValue,
|
|
|
+} from '@/components/ui/select'
|
|
|
+import { PasswordInput } from '@/components/password-input'
|
|
|
+import { useAdminsContext } from './admins-provider'
|
|
|
+
|
|
|
+const schema = z.object({
|
|
|
+ name: z.string().trim().min(1, 'نام الزامی است'),
|
|
|
+ username: z.string().trim().min(1, 'نام کاربری الزامی است'),
|
|
|
+ password: z.string(),
|
|
|
+ status: z.enum(['active', 'inactive']),
|
|
|
+ roles: z.array(z.number()).min(1, 'حداقل یک نقش را انتخاب کنید'),
|
|
|
+})
|
|
|
+
|
|
|
+type FormValues = z.infer<typeof schema>
|
|
|
+
|
|
|
+const emptyValues: FormValues = {
|
|
|
+ name: '',
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ status: 'active',
|
|
|
+ roles: [],
|
|
|
+}
|
|
|
+
|
|
|
+export function AdminsActionDialog() {
|
|
|
+ const { open, setOpen, currentRow, setCurrentRow } = useAdminsContext()
|
|
|
+ const isAdd = open === 'add'
|
|
|
+ const isEdit = open === 'edit'
|
|
|
+ const isView = open === 'view'
|
|
|
+ const opened = isAdd || isEdit || isView
|
|
|
+ const readOnly = isView
|
|
|
+ const { can } = useCan()
|
|
|
+ const canReadRoles = can(PERMISSIONS.roles.view)
|
|
|
+
|
|
|
+ const adminQuery = useAdmin(opened && !isAdd ? currentRow?.id : undefined)
|
|
|
+ const rolesQuery = useRoles(
|
|
|
+ { page: 1, per_page: 100 },
|
|
|
+ opened && canReadRoles
|
|
|
+ )
|
|
|
+ const createMutation = useCreateAdmin()
|
|
|
+ const updateMutation = useUpdateAdmin()
|
|
|
+ const isPending = createMutation.isPending || updateMutation.isPending
|
|
|
+
|
|
|
+ const form = useForm<FormValues>({
|
|
|
+ resolver: zodResolver(schema),
|
|
|
+ defaultValues: emptyValues,
|
|
|
+ })
|
|
|
+
|
|
|
+ const roleOptions = useMemo(() => {
|
|
|
+ const options = new Map<number, string>()
|
|
|
+
|
|
|
+ for (const role of rolesQuery.data?.data.roles ?? []) {
|
|
|
+ options.set(role.id, role.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const role of adminQuery.data?.role_details ?? []) {
|
|
|
+ if (!options.has(role.id)) options.set(role.id, role.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ return [...options.entries()].map(([id, name]) => ({ id, name }))
|
|
|
+ }, [rolesQuery.data, adminQuery.data])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!opened) return
|
|
|
+
|
|
|
+ if (isAdd) {
|
|
|
+ form.reset(emptyValues)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!adminQuery.data) return
|
|
|
+
|
|
|
+ const status = adminQuery.data.status === 'inactive' ? 'inactive' : 'active'
|
|
|
+
|
|
|
+ form.reset({
|
|
|
+ name: adminQuery.data.name,
|
|
|
+ username: adminQuery.data.username,
|
|
|
+ password: '',
|
|
|
+ status,
|
|
|
+ roles: adminQuery.data.role_details.map((role) => role.id),
|
|
|
+ })
|
|
|
+ }, [opened, isAdd, adminQuery.data, form])
|
|
|
+
|
|
|
+ const handleClose = () => {
|
|
|
+ setOpen(null)
|
|
|
+ setCurrentRow(null)
|
|
|
+ form.reset(emptyValues)
|
|
|
+ }
|
|
|
+
|
|
|
+ const onSubmit = (values: FormValues) => {
|
|
|
+ if (readOnly) return
|
|
|
+
|
|
|
+ if (isAdd && !values.password.trim()) {
|
|
|
+ form.setError('password', { message: 'رمز عبور الزامی است' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ name: values.name.trim(),
|
|
|
+ username: values.username.trim(),
|
|
|
+ status: values.status,
|
|
|
+ roles: values.roles,
|
|
|
+ ...(values.password.trim() ? { password: values.password } : {}),
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isAdd) {
|
|
|
+ createMutation.mutate(payload, {
|
|
|
+ onSuccess: () => {
|
|
|
+ toast.success('مدیر با موفقیت ایجاد شد')
|
|
|
+ handleClose()
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ toast.error(getApiErrorMessage(error, 'خطا در ایجاد مدیر'))
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isEdit && currentRow) {
|
|
|
+ updateMutation.mutate(
|
|
|
+ { id: currentRow.id, payload },
|
|
|
+ {
|
|
|
+ onSuccess: () => {
|
|
|
+ toast.success('مدیر با موفقیت ویرایش شد')
|
|
|
+ handleClose()
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ toast.error(getApiErrorMessage(error, 'خطا در ویرایش مدیر'))
|
|
|
+ },
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const isLoading =
|
|
|
+ ((isEdit || isView) && adminQuery.isLoading) ||
|
|
|
+ (canReadRoles && rolesQuery.isLoading)
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Dialog open={opened} onOpenChange={(state) => !state && handleClose()}>
|
|
|
+ <DialogContent className='max-h-[90vh] overflow-y-auto sm:max-w-lg'>
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>
|
|
|
+ {isAdd && 'افزودن مدیر'}
|
|
|
+ {isEdit && 'ویرایش مدیر'}
|
|
|
+ {isView && 'مشاهده مدیر'}
|
|
|
+ </DialogTitle>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ {isLoading ? (
|
|
|
+ <div className='py-10 text-center'>در حال دریافت...</div>
|
|
|
+ ) : adminQuery.isError ? (
|
|
|
+ <div className='py-10 text-center text-sm text-destructive'>
|
|
|
+ دریافت اطلاعات مدیر ممکن نشد.
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <Form {...form}>
|
|
|
+ <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name='name'
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>نام</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ {...field}
|
|
|
+ disabled={readOnly}
|
|
|
+ placeholder='نام مدیر'
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name='username'
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>نام کاربری</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input
|
|
|
+ {...field}
|
|
|
+ disabled={readOnly}
|
|
|
+ placeholder='نام کاربری'
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ {!readOnly && (
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name='password'
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>رمز عبور</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <PasswordInput
|
|
|
+ {...field}
|
|
|
+ placeholder={
|
|
|
+ isEdit
|
|
|
+ ? 'برای تغییر رمز، مقدار جدید را وارد کنید'
|
|
|
+ : 'رمز عبور'
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name='status'
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>وضعیت</FormLabel>
|
|
|
+ <Select
|
|
|
+ disabled={readOnly}
|
|
|
+ value={field.value}
|
|
|
+ onValueChange={field.onChange}
|
|
|
+ >
|
|
|
+ <FormControl>
|
|
|
+ <SelectTrigger className='w-full'>
|
|
|
+ <SelectValue placeholder='انتخاب وضعیت' />
|
|
|
+ </SelectTrigger>
|
|
|
+ </FormControl>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value='active'>فعال</SelectItem>
|
|
|
+ <SelectItem value='inactive'>غیرفعال</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={form.control}
|
|
|
+ name='roles'
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>نقشها</FormLabel>
|
|
|
+ {!canReadRoles && roleOptions.length === 0 ? (
|
|
|
+ <p className='text-sm text-muted-foreground'>
|
|
|
+ برای انتخاب نقش، دسترسی مشاهده نقشها لازم است.
|
|
|
+ </p>
|
|
|
+ ) : rolesQuery.isError ? (
|
|
|
+ <p className='text-sm text-destructive'>
|
|
|
+ دریافت نقشها ممکن نشد.
|
|
|
+ </p>
|
|
|
+ ) : roleOptions.length === 0 ? (
|
|
|
+ <p className='text-sm text-muted-foreground'>
|
|
|
+ نقشی برای انتخاب وجود ندارد.
|
|
|
+ </p>
|
|
|
+ ) : (
|
|
|
+ <div className='grid gap-2 rounded-md border p-3'>
|
|
|
+ {roleOptions.map((role) => (
|
|
|
+ <label
|
|
|
+ key={role.id}
|
|
|
+ className='flex items-center gap-2 text-sm'
|
|
|
+ >
|
|
|
+ <Checkbox
|
|
|
+ checked={field.value.includes(role.id)}
|
|
|
+ disabled={readOnly}
|
|
|
+ onCheckedChange={(checked) => {
|
|
|
+ field.onChange(
|
|
|
+ checked === true
|
|
|
+ ? [...field.value, role.id]
|
|
|
+ : field.value.filter((id) => id !== role.id)
|
|
|
+ )
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ {role.name}
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type='button' variant='outline' onClick={handleClose}>
|
|
|
+ {readOnly ? 'بستن' : 'انصراف'}
|
|
|
+ </Button>
|
|
|
+ {!readOnly && (
|
|
|
+ <Button type='submit' disabled={isPending}>
|
|
|
+ {isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ )}
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+ )
|
|
|
+}
|