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 (
+
+ )
+}
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 (
-
- )
-}
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.
+
+
+
+
+
+
+
+ )
+}