Pārlūkot izejas kodu

feat: add activity field ids in permissions

Mohammad Mahdi Salimi 2 nedēļas atpakaļ
vecāks
revīzija
bcc22104e5

+ 24 - 20
src/features/admins/components/admins-action-dialog.tsx

@@ -293,26 +293,30 @@ export function AdminsActionDialog() {
                         نقشی برای انتخاب وجود ندارد.
                       </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 className='max-h-44 overflow-y-auto rounded-md border p-3'>
+                        <div className='grid gap-2'>
+                          {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>
                       </div>
                     )}
                     <FormMessage />

+ 69 - 0
src/features/roles/components/activity-field-picker.tsx

@@ -0,0 +1,69 @@
+import { Checkbox } from '@/components/ui/checkbox'
+
+type ActivityFieldOption = {
+  id: number
+  name: string
+}
+
+type ActivityFieldPickerProps = {
+  fields: ActivityFieldOption[]
+  value: number[]
+  onChange: (value: number[]) => void
+  disabled?: boolean
+}
+
+export function ActivityFieldPicker({
+  fields,
+  value,
+  onChange,
+  disabled = false,
+}: ActivityFieldPickerProps) {
+  const selected = new Set(value)
+  const selectedCount = fields.filter((field) => selected.has(field.id)).length
+  const allChecked = fields.length > 0 && selectedCount === fields.length
+
+  const toggle = (id: number, checked: boolean) => {
+    onChange(
+      checked ? [...value, id] : value.filter((currentId) => currentId !== id)
+    )
+  }
+
+  return (
+    <div className='overflow-hidden rounded-md border'>
+      <div className='flex items-center justify-between gap-3 border-b px-3 py-2'>
+        <label className='flex items-center gap-2 text-sm font-medium'>
+          <Checkbox
+            checked={
+              allChecked ? true : selectedCount > 0 ? 'indeterminate' : false
+            }
+            disabled={disabled || fields.length === 0}
+            onCheckedChange={(checked) =>
+              onChange(checked === true ? fields.map((field) => field.id) : [])
+            }
+          />
+          همه رشته‌ها
+        </label>
+        <span className='text-xs text-muted-foreground'>
+          {selectedCount} از {fields.length}
+        </span>
+      </div>
+
+      <div className='max-h-44 overflow-y-auto p-3'>
+        <div className='grid gap-2 sm:grid-cols-2'>
+          {fields.map((field) => (
+            <label key={field.id} className='flex items-center gap-2 text-sm'>
+              <Checkbox
+                checked={selected.has(field.id)}
+                disabled={disabled}
+                onCheckedChange={(checked) =>
+                  toggle(field.id, checked === true)
+                }
+              />
+              {field.name}
+            </label>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 63 - 10
src/features/roles/components/roles-action-dialog.tsx

@@ -1,9 +1,10 @@
-import { useEffect } from 'react'
+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 { useActivityFields } from '@/hooks/activity-fields/useActivityFields'
 import { usePermissionModules } from '@/hooks/permissions/usePermissionModules'
 import { useCreateRole } from '@/hooks/roles/useCreateRole'
 import { useRole } from '@/hooks/roles/useRole'
@@ -25,13 +26,16 @@ import {
   FormMessage,
 } from '@/components/ui/form'
 import { Input } from '@/components/ui/input'
-import { ScrollArea } from '@/components/ui/scroll-area'
+import { ActivityFieldPicker } from './activity-field-picker'
 import { PermissionPicker } from './permission-picker'
 import { useRolesContext } from './roles-provider'
 
 const schema = z.object({
   name: z.string().trim().min(1, 'نام نقش الزامی است'),
   permissions: z.array(z.string()).min(1, 'حداقل یک دسترسی را انتخاب کنید'),
+  activity_field_ids: z
+    .array(z.number())
+    .min(1, 'حداقل یک رشته را انتخاب کنید'),
 })
 
 type FormValues = z.infer<typeof schema>
@@ -46,6 +50,7 @@ export function RolesActionDialog() {
 
   const roleQuery = useRole(opened && !isAdd ? currentRow?.id : undefined)
   const modulesQuery = usePermissionModules(opened)
+  const activityFieldsQuery = useActivityFields(opened)
   const createMutation = useCreateRole()
   const updateMutation = useUpdateRole()
   const isPending = createMutation.isPending || updateMutation.isPending
@@ -55,6 +60,7 @@ export function RolesActionDialog() {
     defaultValues: {
       name: '',
       permissions: [],
+      activity_field_ids: [],
     },
   })
 
@@ -62,7 +68,7 @@ export function RolesActionDialog() {
     if (!opened) return
 
     if (isAdd) {
-      form.reset({ name: '', permissions: [] })
+      form.reset({ name: '', permissions: [], activity_field_ids: [] })
       return
     }
 
@@ -73,13 +79,16 @@ export function RolesActionDialog() {
       permissions: roleQuery.data.permissions.map(
         (permission) => permission.name
       ),
+      activity_field_ids: roleQuery.data.activity_field_ids?.length
+        ? roleQuery.data.activity_field_ids
+        : (roleQuery.data.activity_fields ?? []).map((field) => field.id),
     })
   }, [opened, isAdd, roleQuery.data, form])
 
   const handleClose = () => {
     setOpen(null)
     setCurrentRow(null)
-    form.reset({ name: '', permissions: [] })
+    form.reset({ name: '', permissions: [], activity_field_ids: [] })
   }
 
   const onSubmit = (values: FormValues) => {
@@ -88,6 +97,7 @@ export function RolesActionDialog() {
     const payload = {
       name: values.name.trim(),
       permissions: values.permissions,
+      activity_field_ids: values.activity_field_ids,
     }
 
     if (isAdd) {
@@ -118,8 +128,24 @@ export function RolesActionDialog() {
     }
   }
 
+  const activityFieldOptions = useMemo(() => {
+    const options = new Map<number, string>()
+
+    for (const field of activityFieldsQuery.data ?? []) {
+      options.set(field.id, field.name)
+    }
+
+    for (const field of roleQuery.data?.activity_fields ?? []) {
+      if (!options.has(field.id)) options.set(field.id, field.name)
+    }
+
+    return [...options.entries()].map(([id, name]) => ({ id, name }))
+  }, [activityFieldsQuery.data, roleQuery.data])
+
   const isLoading =
-    modulesQuery.isLoading || ((isEdit || isView) && roleQuery.isLoading)
+    modulesQuery.isLoading ||
+    activityFieldsQuery.isLoading ||
+    ((isEdit || isView) && roleQuery.isLoading)
 
   return (
     <Dialog open={opened} onOpenChange={(state) => !state && handleClose()}>
@@ -142,7 +168,7 @@ export function RolesActionDialog() {
           <Form {...form}>
             <form
               onSubmit={form.handleSubmit(onSubmit)}
-              className='flex min-h-0 flex-1 flex-col gap-4'
+              className='flex min-h-0 flex-1 flex-col gap-4 overflow-hidden'
             >
               <FormField
                 control={form.control}
@@ -162,26 +188,53 @@ export function RolesActionDialog() {
                 )}
               />
 
+              <FormField
+                control={form.control}
+                name='activity_field_ids'
+                render={({ field }) => (
+                  <FormItem className='shrink-0'>
+                    <FormLabel>رشته‌ها</FormLabel>
+                    {activityFieldsQuery.isError ? (
+                      <p className='text-sm text-destructive'>
+                        دریافت رشته‌ها ممکن نشد.
+                      </p>
+                    ) : activityFieldOptions.length === 0 ? (
+                      <p className='text-sm text-muted-foreground'>
+                        رشته‌ای برای انتخاب وجود ندارد.
+                      </p>
+                    ) : (
+                      <ActivityFieldPicker
+                        fields={activityFieldOptions}
+                        value={field.value}
+                        onChange={field.onChange}
+                        disabled={readOnly}
+                      />
+                    )}
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
               <FormField
                 control={form.control}
                 name='permissions'
                 render={({ field }) => (
-                  <FormItem className='min-h-0 flex-1'>
+                  <FormItem className='flex min-h-0 flex-1 flex-col'>
                     <FormLabel>دسترسی‌ها</FormLabel>
-                    <ScrollArea className='h-[50vh] rounded-md border p-3'>
+                    <div className='min-h-48 flex-1 overflow-y-auto rounded-md border p-3'>
                       <PermissionPicker
                         modules={modulesQuery.data ?? []}
                         value={field.value}
                         onChange={field.onChange}
                         disabled={readOnly}
                       />
-                    </ScrollArea>
+                    </div>
                     <FormMessage />
                   </FormItem>
                 )}
               />
 
-              <DialogFooter>
+              <DialogFooter className='shrink-0'>
                 <Button type='button' variant='outline' onClick={handleClose}>
                   {readOnly ? 'بستن' : 'انصراف'}
                 </Button>

+ 29 - 0
src/features/roles/components/roles-columns.tsx

@@ -40,6 +40,35 @@ export const rolesColumns: ColumnDef<Role>[] = [
       </Badge>
     ),
   },
+  {
+    id: 'activity_fields',
+    accessorFn: (row) =>
+      row.activity_fields?.map((field) => field.name).join(', ') ?? '',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='رشته‌ها' />
+    ),
+    cell: ({ row }) => {
+      const fields = row.original.activity_fields ?? []
+
+      if (fields.length === 0) {
+        return <span className='text-sm text-muted-foreground'>—</span>
+      }
+
+      const visibleFields = fields.length > 3 ? fields.slice(0, 2) : fields
+      const hiddenCount = fields.length - visibleFields.length
+
+      return (
+        <div className='flex max-w-64 flex-nowrap items-center gap-1'>
+          {visibleFields.map((field) => (
+            <Badge key={field.id} variant='outline'>
+              {field.name}
+            </Badge>
+          ))}
+          {hiddenCount > 0 && <Badge variant='secondary'>+{hiddenCount}</Badge>}
+        </div>
+      )
+    },
+  },
   {
     id: 'permissions_count',
     accessorFn: (row) => row.permissions.length,

+ 16 - 0
src/types/roles.types.ts

@@ -9,6 +9,19 @@ export interface RolePermission {
   }
 }
 
+export interface RoleActivityField {
+  id: number
+  name: string
+  slug: string
+  tag: string
+  pivot?: {
+    role_id: number
+    activity_field_id: number
+    created_at?: string
+    updated_at?: string
+  }
+}
+
 export interface Role {
   id: number
   name: string
@@ -16,7 +29,9 @@ export interface Role {
   guard_name: string
   created_at: string
   updated_at: string
+  activity_field_ids: number[]
   permissions: RolePermission[]
+  activity_fields: RoleActivityField[]
 }
 
 export interface RolesListResponse {
@@ -39,4 +54,5 @@ export interface RoleDetailResponse {
 export interface RolePayload {
   name: string
   permissions: string[]
+  activity_field_ids: number[]
 }