From 6b608ad2efc1ef5d38f92fa7cbe0e5a09f5a449d Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Mon, 19 Oct 2026 16:39:47 +0530 Subject: [PATCH] Update permission feature with edit and create role functionality --- .../_components/analytics-sections.tsx | 19 +- .../users/roles/[id]/edit/page.tsx | 54 +++++ .../users/roles/_components/role-form.tsx | 208 +++++++++++++++++ .../users/roles/_components/role-sheet.tsx | 217 ------------------ .../users/roles/_components/roles-client.tsx | 16 +- .../app/(dashboard)/users/roles/new/page.tsx | 32 +++ 6 files changed, 311 insertions(+), 235 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/users/roles/[id]/edit/page.tsx create mode 100644 distribution_fe/app/(dashboard)/users/roles/_components/role-form.tsx delete mode 100644 distribution_fe/app/(dashboard)/users/roles/_components/role-sheet.tsx create mode 100644 distribution_fe/app/(dashboard)/users/roles/new/page.tsx diff --git a/distribution_fe/app/(dashboard)/_components/analytics-sections.tsx b/distribution_fe/app/(dashboard)/_components/analytics-sections.tsx index e3ef94a..34e55d5 100644 --- a/distribution_fe/app/(dashboard)/_components/analytics-sections.tsx +++ b/distribution_fe/app/(dashboard)/_components/analytics-sections.tsx @@ -59,8 +59,15 @@ function Kpi({ title, value, footer }: { title: string; value: string; footer?: function LoadingGrid() { return ( -
- {Array.from({ length: 8 }).map((_, i) => )} +
+
+ {Array.from({ length: 6 }).map((_, i) => )} +
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
) } @@ -161,11 +168,11 @@ const sharePct = (v: number, total: number) => (total > 0 ? `${((v / total) * 10 export function SalesSection({ range }: { range: DateRange }) { const { data: sales, loading } = useLoad( () => dashboardReports.sales(range), [range.from, range.to], 'Failed to load sales analytics') - const { data: products } = useLoad( + const { data: products, loading: productsLoading } = useLoad( () => productReports.sales({ ...range, pageSize: 10 }).then(r => getPaginatedItems(r)), [range.from, range.to], 'Failed to load product sales') - if (loading || !sales) return + if (loading || productsLoading || !sales) return const { current: c, previous: p } = sales.kpis const kpi = (title: string, key: keyof SalesKpis, money = true) => ( @@ -252,11 +259,11 @@ export function InventorySection({ range }: { range: DateRange }) { const { data: lowStock } = useLoad( () => productReports.stock({ lowStockOnly: true, pageSize: 50 }).then(r => getPaginatedItems(r)), [], 'Failed to load low-stock products') - const { data: movers } = useLoad( + const { data: movers, loading: moversLoading } = useLoad( () => productReports.sales({ ...range, pageSize: 50 }).then(r => getPaginatedItems(r)), [range.from, range.to], 'Failed to load product sales') - if (loading || !inv || !overview) return + if (loading || moversLoading || !inv || !overview) return const healthy = Math.max(0, overview.totalSkus - overview.lowStockCount - overview.outOfStockCount) const sold = (movers ?? []).map(r => ({ name: r.name, value: r.qtyFulfilled })) diff --git a/distribution_fe/app/(dashboard)/users/roles/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/users/roles/[id]/edit/page.tsx new file mode 100644 index 0000000..4150709 --- /dev/null +++ b/distribution_fe/app/(dashboard)/users/roles/[id]/edit/page.tsx @@ -0,0 +1,54 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { HugeiconsIcon } from '@hugeicons/react' +import { ArrowLeft01Icon } from '@hugeicons/core-free-icons' + +import { Button } from '@/components/ui/button' +import { toast } from '@/components/ui/toast' +import { roles as rolesApi, getApiError } from '@/lib/api/index' +import type { RoleResponse } from '@/types/api' + +import { RoleForm } from '../../_components/role-form' + +export default function EditRolePage() { + const { id } = useParams<{ id: string }>() + const router = useRouter() + const goBack = () => router.push('/users/roles') + + const [role, setRole] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + rolesApi.get(id) + .then(setRole) + .catch((err) => toast.add({ title: 'Failed to load role', description: getApiError(err), type: 'error' })) + .finally(() => setLoading(false)) + }, [id]) + + return ( +
+
+ +
+

Edit Role

+

Update role details and permissions.

+
+
+ +
+ {loading && ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ )} + {!loading && role && } +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/users/roles/_components/role-form.tsx b/distribution_fe/app/(dashboard)/users/roles/_components/role-form.tsx new file mode 100644 index 0000000..ab5b38b --- /dev/null +++ b/distribution_fe/app/(dashboard)/users/roles/_components/role-form.tsx @@ -0,0 +1,208 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useForm, zodResolver, z } from '@/lib/form' +import { FormField } from '@/components/shared/form-field' +import { Input } from '@/components/ui/input' +import { Checkbox } from '@/components/ui/checkbox' +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select' +import { Button } from '@/components/ui/button' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { roles as rolesApi, permissions as permissionsApi, getApiError } from '@/lib/api/index' +import type { RoleResponse, PermissionResponse } from '@/types/api' +import { PERMISSION_CATEGORIES } from '@/config/permissions' + +const ROLE_LEVELS: { value: number; label: string; description: string }[] = [ + { value: 10, label: 'Level 10 — Field Agent', description: 'Front-line sales rep; can place orders and check in to visits' }, + { value: 20, label: 'Level 20 — Supervisor', description: 'Oversees agents; can approve variances and update store credit limits' }, + { value: 30, label: 'Level 30 — Area Manager', description: 'Manages a group of supervisors within an area' }, + { value: 40, label: 'Level 40 — Area Supervisor', description: 'Senior oversight across multiple areas' }, + { value: 50, label: 'Level 50 — Regional Manager', description: 'Controls region targets, budgets and route assignments' }, + { value: 60, label: 'Level 60 — Regional Agency', description: 'Agency-level access across any region; broader scope than Regional Manager' }, + { value: 100, label: 'Level 100 — Super Admin', description: 'Full system access; all permissions granted' }, +] + +const schema = z.object({ + name: z.string().min(1, 'Name is required'), + level: z.string().min(1, 'Level is required'), +}) + +type FormValues = z.infer + +// 'orders.view' -> 'View Orders', 'loading-sheet.manage' -> 'Manage Loading Sheet' +function permissionLabel(code: string): string { + const [resource, action] = code.split('.') + const title = (s: string) => s.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') + return action ? `${title(action)} ${title(resource)}` : title(resource) +} + +interface RoleFormProps { + role: RoleResponse | null + onSuccess: () => void + onCancel: () => void + wide?: boolean +} + +export function RoleForm({ role, onSuccess, onCancel, wide = false }: RoleFormProps) { + const isEdit = !!role + + const [allPermissions, setAllPermissions] = useState([]) + const [selectedIds, setSelectedIds] = useState>(new Set()) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { name: '', level: '' }, + }) + + useEffect(() => { + permissionsApi.list().then(setAllPermissions).catch(() => {}) + if (role) { + form.reset({ name: role.name, level: String(role.level) }) + rolesApi.getPermissions(role.id) + .then((perms) => setSelectedIds(new Set(perms.map((p) => p.permissionId)))) + .catch(() => {}) + } else { + form.reset({ name: '', level: '' }) + setSelectedIds(new Set()) + } + }, [role]) + + function togglePermission(id: string) { + setSelectedIds((prev) => { + const next = new Set(prev) + next.has(id) ? next.delete(id) : next.add(id) + return next + }) + } + + async function onSubmit(values: FormValues) { + const body = { name: values.name, level: parseInt(values.level, 10) } + try { + let roleId: string + if (isEdit) { + await rolesApi.update(role!.id, body) + roleId = role!.id + toast.add({ title: 'Role updated', type: 'success' }) + } else { + const created = await rolesApi.create(body) + roleId = created.id + toast.add({ title: 'Role created', type: 'success' }) + } + await rolesApi.assignPermissions(roleId, Array.from(selectedIds)) + onSuccess() + } catch (err) { + toast.add({ title: 'Failed to save role', description: getApiError(err), type: 'error' }) + } + } + + return ( +
+ + ( + + )} + /> + ( + + )} + /> + + + {allPermissions.length > 0 && ( +
+

Permissions

+
+
+ {PERMISSION_CATEGORIES.map((cat) => { + const catPerms = allPermissions.filter((p) => cat.codes.includes(p.code)) + if (catPerms.length === 0) return null + const allChecked = catPerms.every((p) => selectedIds.has(p.permissionId)) + const someChecked = catPerms.some((p) => selectedIds.has(p.permissionId)) + return ( +
+
+ { + setSelectedIds((prev) => { + const next = new Set(prev) + catPerms.forEach((p) => checked ? next.add(p.permissionId) : next.delete(p.permissionId)) + return next + }) + }} + /> + +
+
+ {catPerms.map((p) => ( +
+ togglePermission(p.permissionId)} + className="mt-0.5" + /> + +
+ ))} +
+
+ ) + })} +
+
+
+ )} + +
+ + +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/users/roles/_components/role-sheet.tsx b/distribution_fe/app/(dashboard)/users/roles/_components/role-sheet.tsx deleted file mode 100644 index ed51e61..0000000 --- a/distribution_fe/app/(dashboard)/users/roles/_components/role-sheet.tsx +++ /dev/null @@ -1,217 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { useForm, zodResolver, z } from '@/lib/form' -import { FormField } from '@/components/shared/form-field' -import { Input } from '@/components/ui/input' -import { Checkbox } from '@/components/ui/checkbox' -import { - Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select' -import { Button } from '@/components/ui/button' -import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, -} from '@/components/ui/dialog' -import { FieldGroup } from '@/components/ui/field' -import { toast } from '@/components/ui/toast' -import { roles as rolesApi, permissions as permissionsApi, getApiError } from '@/lib/api/index' -import type { RoleResponse, PermissionResponse } from '@/types/api' -import { PERMISSION_CATEGORIES } from '@/config/permissions' - -const ROLE_LEVELS: { value: number; label: string; description: string }[] = [ - { value: 10, label: 'Level 10 — Field Agent', description: 'Front-line sales rep; can place orders and check in to visits' }, - { value: 20, label: 'Level 20 — Supervisor', description: 'Oversees agents; can approve variances and update store credit limits' }, - { value: 30, label: 'Level 30 — Area Manager', description: 'Manages a group of supervisors within an area' }, - { value: 40, label: 'Level 40 — Area Supervisor', description: 'Senior oversight across multiple areas' }, - { value: 50, label: 'Level 50 — Regional Manager', description: 'Controls region targets, budgets and route assignments' }, - { value: 60, label: 'Level 60 — Regional Agency', description: 'Agency-level access across any region; broader scope than Regional Manager' }, - { value: 100, label: 'Level 100 — Super Admin', description: 'Full system access; all permissions granted' }, -] - -const schema = z.object({ - name: z.string().min(1, 'Name is required'), - level: z.string().min(1, 'Level is required'), -}) - -type FormValues = z.infer - -interface RoleSheetProps { - open: boolean - onClose: () => void - role: RoleResponse | null - onSuccess: () => void -} - -export function RoleSheet({ open, onClose, role, onSuccess }: RoleSheetProps) { - const isEdit = !!role - - const [allPermissions, setAllPermissions] = useState([]) - const [selectedIds, setSelectedIds] = useState>(new Set()) - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { name: '', level: '' }, - }) - - useEffect(() => { - if (!open) return - permissionsApi.list().then(setAllPermissions).catch(() => {}) - if (role) { - form.reset({ name: role.name, level: String(role.level) }) - rolesApi.getPermissions(role.id) - .then((perms) => setSelectedIds(new Set(perms.map((p) => p.permissionId)))) - .catch(() => {}) - } else { - form.reset({ name: '', level: '' }) - setSelectedIds(new Set()) - } - }, [open, role]) - - function togglePermission(id: string) { - setSelectedIds((prev) => { - const next = new Set(prev) - next.has(id) ? next.delete(id) : next.add(id) - return next - }) - } - - async function onSubmit(values: FormValues) { - const body = { name: values.name, level: parseInt(values.level, 10) } - try { - let roleId: string - if (isEdit) { - await rolesApi.update(role!.id, body) - roleId = role!.id - toast.add({ title: 'Role updated', type: 'success' }) - } else { - const created = await rolesApi.create(body) - roleId = created.id - toast.add({ title: 'Role created', type: 'success' }) - } - await rolesApi.assignPermissions(roleId, Array.from(selectedIds)) - onSuccess() - onClose() - } catch (err) { - toast.add({ title: 'Failed to save role', description: getApiError(err), type: 'error' }) - } - } - - return ( - !v && onClose()}> - - - {isEdit ? 'Edit Role' : 'Add Role'} - - {isEdit ? 'Update role details and permissions.' : 'Create a new role and assign permissions.'} - - - -
- - ( - - )} - /> - ( - - )} - /> - - - {allPermissions.length > 0 && ( -
-

Permissions

-
-
- {PERMISSION_CATEGORIES.map((cat) => { - const catPerms = allPermissions.filter((p) => cat.codes.includes(p.code)) - if (catPerms.length === 0) return null - const allChecked = catPerms.every((p) => selectedIds.has(p.permissionId)) - const someChecked = catPerms.some((p) => selectedIds.has(p.permissionId)) - return ( -
-
- { - setSelectedIds((prev) => { - const next = new Set(prev) - catPerms.forEach((p) => checked ? next.add(p.permissionId) : next.delete(p.permissionId)) - return next - }) - }} - /> - -
-
- {catPerms.map((p) => ( -
- togglePermission(p.permissionId)} - className="mt-0.5" - /> - -
- ))} -
-
- ) - })} -
-
-
- )} - - - - - -
-
-
- ) -} diff --git a/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx b/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx index 108cd7d..f757c5e 100644 --- a/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx +++ b/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useCallback, useMemo } from 'react' +import { useRouter } from 'next/navigation' import { HugeiconsIcon } from '@hugeicons/react' import { Add01Icon } from '@hugeicons/core-free-icons' @@ -12,17 +13,15 @@ import { toast } from '@/components/ui/toast' import { roles as rolesApi, getApiError } from '@/lib/api/index' import type { RoleResponse } from '@/types/api' -import { RoleSheet } from './role-sheet' import { RoleViewSheet } from './role-view-sheet' export function RolesClient() { + const router = useRouter() const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [pageSize, setPageSize] = useState(20) - const [sheetOpen, setSheetOpen] = useState(false) - const [editing, setEditing] = useState(null) const [viewOpen, setViewOpen] = useState(false) const [viewing, setViewing] = useState(null) @@ -41,8 +40,8 @@ export function RolesClient() { useEffect(() => { fetchData() }, [fetchData]) - function openCreate() { setEditing(null); setSheetOpen(true) } - function openEdit(role: RoleResponse) { setEditing(role); setSheetOpen(true) } + function openCreate() { router.push('/users/roles/new') } + function openEdit(role: RoleResponse) { router.push(`/users/roles/${role.id}/edit`) } function openView(role: RoleResponse) { setViewing(role); setViewOpen(true) } async function handleDeactivate(role: RoleResponse) { @@ -122,13 +121,6 @@ export function RolesClient() { onServerSideChange={({ page: p }) => setPageSize(p.pageSize)} /> - setSheetOpen(false)} - role={editing} - onSuccess={() => fetchData()} - /> - setViewOpen(false)} diff --git a/distribution_fe/app/(dashboard)/users/roles/new/page.tsx b/distribution_fe/app/(dashboard)/users/roles/new/page.tsx new file mode 100644 index 0000000..87e0fb6 --- /dev/null +++ b/distribution_fe/app/(dashboard)/users/roles/new/page.tsx @@ -0,0 +1,32 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { HugeiconsIcon } from '@hugeicons/react' +import { ArrowLeft01Icon } from '@hugeicons/core-free-icons' + +import { Button } from '@/components/ui/button' + +import { RoleForm } from '../_components/role-form' + +export default function NewRolePage() { + const router = useRouter() + const goBack = () => router.push('/users/roles') + + return ( +
+
+ +
+

Add Role

+

Create a new role and assign permissions.

+
+
+ +
+ +
+
+ ) +}