feat: add user and role view sheets with detailed information display

- Created UserViewSheet component to display user details including username, full name, role, status, and timestamps.
- Created RoleViewSheet component to display role details including name, level, status, creation date, and associated permissions.
- Introduced permissions configuration in a new permissions.ts file, defining categories and permission codes for better management.
- Implemented middleware for authentication, ensuring access control for protected routes.
This commit is contained in:
Kalana B. Thilakarathna
2026-10-18 23:11:01 +05:30
parent 558afc6f9d
commit 0052725b88
86 changed files with 41117 additions and 939 deletions
@@ -21,6 +21,7 @@ import {
getApiError,
getPaginatedItems,
} from '@/lib/api/index'
import { useAuth } from '@/contexts/auth-context'
import type { LowStockItemResponse, StockMovementResponse } from '@/types/api'
const STORAGE_KEY = 'distribution-dashboard-widgets-v3'
@@ -38,6 +39,7 @@ interface WidgetMeta {
cat: Category
title: string
size: Size
permission: string
}
const CATEGORY_META: Record<Category, { label: string; text: string; bg: string; solid: string }> = {
@@ -48,17 +50,17 @@ const CATEGORY_META: Record<Category, { label: string; text: string; bg: string;
}
const WIDGET_META: WidgetMeta[] = [
{ id: 'pendingPO', cat: 'sales', title: 'Pending Shop POs', size: 'sm' },
{ id: 'awaitingConfirm', cat: 'sales', title: 'Awaiting Confirmation', size: 'sm' },
{ id: 'fulfillRate', cat: 'sales', title: 'Fulfillment Rate', size: 'md' },
{ id: 'openRides', cat: 'ops', title: 'Open Rides', size: 'md' },
{ id: 'loadingSheetsByStatus', cat: 'ops', title: 'Loading Sheets by Status', size: 'md' },
{ id: 'deliveryTasks', cat: 'ops', title: 'Pending Delivery Tasks', size: 'sm' },
{ id: 'lowStock', cat: 'stock', title: 'Low Stock Alerts', size: 'lg' },
{ id: 'stockMovements', cat: 'stock', title: 'Recent Stock Movements', size: 'lg' },
{ id: 'warehouseBalance', cat: 'stock', title: 'Warehouse Balances', size: 'md' },
{ id: 'outstandingPay', cat: 'finance', title: 'Outstanding Payments', size: 'sm' },
{ id: 'invoiceStatus', cat: 'finance', title: 'Invoices by Status', size: 'md' },
{ id: 'pendingPO', cat: 'sales', title: 'Pending Shop POs', size: 'sm', permission: 'orders.view' },
{ id: 'awaitingConfirm', cat: 'sales', title: 'Awaiting Confirmation', size: 'sm', permission: 'orders.view' },
{ id: 'fulfillRate', cat: 'sales', title: 'Fulfillment Rate', size: 'md', permission: 'orders.view' },
{ id: 'openRides', cat: 'ops', title: 'Open Rides', size: 'md', permission: 'rides.view' },
{ id: 'loadingSheetsByStatus',cat: 'ops', title: 'Loading Sheets by Status', size: 'md', permission: 'loading-sheet.view' },
{ id: 'deliveryTasks', cat: 'ops', title: 'Pending Delivery Tasks', size: 'sm', permission: 'delivery-task.view' },
{ id: 'lowStock', cat: 'stock', title: 'Low Stock Alerts', size: 'lg', permission: 'stock.view' },
{ id: 'stockMovements', cat: 'stock', title: 'Recent Stock Movements', size: 'lg', permission: 'stock.view' },
{ id: 'warehouseBalance', cat: 'stock', title: 'Warehouse Balances', size: 'md', permission: 'stock.view' },
{ id: 'outstandingPay', cat: 'finance', title: 'Outstanding Payments', size: 'sm', permission: 'orders.view' },
{ id: 'invoiceStatus', cat: 'finance', title: 'Invoices by Status', size: 'md', permission: 'orders.view' },
]
const DEFAULT_ACTIVE = [
@@ -200,68 +202,137 @@ interface DashboardData {
finalizedInvoiceCount: number
}
async function loadDashboardData(): Promise<DashboardData> {
const [
pendingPO, awaitingConfirm, allPO, fulfilledPO,
openRidesRes,
plannedSheets, confirmedSheets,
pendingTasks, partialTasks,
lowStock,
movements,
balances,
outstanding,
draftInv, printedInv, finalizedInv,
] = await Promise.all([
shopPos.list({ status: 'Pending', pageSize: 1 }),
shopPos.list({ confirmed: true, status: 'Pending', pageSize: 1 }),
shopPos.list({ pageSize: 1 }),
shopPos.list({ status: 'Fulfilled', pageSize: 1 }),
rides.list({ status: 'Open', pageSize: 5 }),
loadingSheets.list({ status: 'Planned', pageSize: 1 }),
loadingSheets.list({ status: 'Confirmed', pageSize: 1 }),
deliveryTasks.list({ status: 'Pending', pageSize: 1 }),
deliveryTasks.list({ status: 'PartiallyDelivered', pageSize: 1 }),
stock.lowStock(),
stock.movements({ pageSize: 5 }),
stock.balances({ locationType: 'Warehouse', pageSize: 300 }),
invoices.outstandingSummary(),
invoices.list({ status: 'Draft', pageSize: 1 }),
invoices.list({ status: 'Printed', pageSize: 1 }),
invoices.list({ status: 'Finalized', pageSize: 1 }),
])
const balanceRows = getPaginatedItems(balances)
const byWarehouse = new Map<string, WarehouseBalanceRow>()
for (const b of balanceRows) {
const existing = byWarehouse.get(b.locationId)
if (existing) existing.quantity += b.quantity
else byWarehouse.set(b.locationId, { locationId: b.locationId, name: b.locationName, quantity: b.quantity })
}
const warehouseBalances = Array.from(byWarehouse.values())
.sort((a, b) => b.quantity - a.quantity)
.slice(0, 5)
function emptyDashboardData(): DashboardData {
return {
pendingPOCount: pendingPO.total,
awaitingConfirmCount: awaitingConfirm.total,
totalPOCount: allPO.total,
fulfilledPOCount: fulfilledPO.total,
openRides: getPaginatedItems(openRidesRes).map(r => ({ id: r.id, vehicleNumberPlate: r.vehicleNumberPlate, driverFullName: r.driverFullName })),
plannedSheetsCount: plannedSheets.total,
confirmedSheetsCount: confirmedSheets.total,
pendingTasksCount: pendingTasks.total,
partialTasksCount: partialTasks.total,
lowStock: getPaginatedItems(lowStock).slice(0, 5),
movements: getPaginatedItems(movements),
warehouseBalances,
outstandingAmount: outstanding.outstandingAmount,
outstandingInvoiceCount: outstanding.invoiceCount,
draftInvoiceCount: draftInv.total,
printedInvoiceCount: printedInv.total,
finalizedInvoiceCount: finalizedInv.total,
pendingPOCount: 0, awaitingConfirmCount: 0, totalPOCount: 0, fulfilledPOCount: 0,
openRides: [],
plannedSheetsCount: 0, confirmedSheetsCount: 0,
pendingTasksCount: 0, partialTasksCount: 0,
lowStock: [], movements: [], warehouseBalances: [],
outstandingAmount: 0, outstandingInvoiceCount: 0,
draftInvoiceCount: 0, printedInvoiceCount: 0, finalizedInvoiceCount: 0,
}
}
async function loadDashboardData(activeIds: Set<string>): Promise<DashboardData> {
const data = emptyDashboardData()
const fetches: Promise<void>[] = []
if (activeIds.has('pendingPO') || activeIds.has('awaitingConfirm') || activeIds.has('fulfillRate')) {
fetches.push(
Promise.all([
shopPos.list({ status: 'Pending', pageSize: 1 }),
shopPos.list({ confirmed: true, status: 'Pending', pageSize: 1 }),
shopPos.list({ pageSize: 1 }),
shopPos.list({ status: 'Fulfilled', pageSize: 1 }),
]).then(([pending, awaiting, all, fulfilled]) => {
data.pendingPOCount = pending.total
data.awaitingConfirmCount = awaiting.total
data.totalPOCount = all.total
data.fulfilledPOCount = fulfilled.total
}).catch(() => {})
)
}
if (activeIds.has('openRides')) {
fetches.push(
rides.list({ status: 'Open', pageSize: 5 })
.then(res => {
data.openRides = getPaginatedItems(res).map(r => ({
id: r.id, vehicleNumberPlate: r.vehicleNumberPlate, driverFullName: r.driverFullName,
}))
})
.catch(() => {})
)
}
if (activeIds.has('loadingSheetsByStatus')) {
fetches.push(
Promise.all([
loadingSheets.list({ status: 'Planned', pageSize: 1 }),
loadingSheets.list({ status: 'Confirmed', pageSize: 1 }),
]).then(([planned, confirmed]) => {
data.plannedSheetsCount = planned.total
data.confirmedSheetsCount = confirmed.total
}).catch(() => {})
)
}
if (activeIds.has('deliveryTasks')) {
fetches.push(
Promise.all([
deliveryTasks.list({ status: 'Pending', pageSize: 1 }),
deliveryTasks.list({ status: 'PartiallyDelivered', pageSize: 1 }),
]).then(([pending, partial]) => {
data.pendingTasksCount = pending.total
data.partialTasksCount = partial.total
}).catch(() => {})
)
}
if (activeIds.has('lowStock')) {
fetches.push(
stock.lowStock()
.then(res => { data.lowStock = getPaginatedItems(res).slice(0, 5) })
.catch(() => {})
)
}
if (activeIds.has('stockMovements')) {
fetches.push(
stock.movements({ pageSize: 5 })
.then(res => { data.movements = getPaginatedItems(res) })
.catch(() => {})
)
}
if (activeIds.has('warehouseBalance')) {
fetches.push(
stock.balances({ locationType: 'Warehouse', pageSize: 300 })
.then(res => {
const byWarehouse = new Map<string, WarehouseBalanceRow>()
for (const b of getPaginatedItems(res)) {
const existing = byWarehouse.get(b.locationId)
if (existing) existing.quantity += b.quantity
else byWarehouse.set(b.locationId, { locationId: b.locationId, name: b.locationName, quantity: b.quantity })
}
data.warehouseBalances = Array.from(byWarehouse.values())
.sort((a, b) => b.quantity - a.quantity)
.slice(0, 5)
})
.catch(() => {})
)
}
if (activeIds.has('outstandingPay')) {
fetches.push(
invoices.outstandingSummary()
.then(res => {
data.outstandingAmount = res.outstandingAmount
data.outstandingInvoiceCount = res.invoiceCount
})
.catch(() => {})
)
}
if (activeIds.has('invoiceStatus')) {
fetches.push(
Promise.all([
invoices.list({ status: 'Draft', pageSize: 1 }),
invoices.list({ status: 'Printed', pageSize: 1 }),
invoices.list({ status: 'Finalized', pageSize: 1 }),
]).then(([draft, printed, finalized]) => {
data.draftInvoiceCount = draft.total
data.printedInvoiceCount = printed.total
data.finalizedInvoiceCount = finalized.total
}).catch(() => {})
)
}
await Promise.all(fetches)
return data
}
function CategoryPill({ cat }: { cat: Category }) {
const meta = CATEGORY_META[cat]
return (
@@ -462,6 +533,7 @@ function mergeLayoutsChange(
}
export function DashboardClient() {
const { hasPermission } = useAuth()
const [data, setData] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true)
const [state, setState] = useState<DashboardLayoutState>(defaultLayoutState)
@@ -470,6 +542,16 @@ export function DashboardClient() {
const [category, setCategory] = useState<'all' | Category>('all')
const { width, containerRef, mounted } = useContainerWidth({ initialWidth: 1200 })
const allowedIds = useMemo(
() => new Set(WIDGET_META.filter(w => hasPermission(w.permission)).map(w => w.id)),
[hasPermission],
)
const permittedActiveIds = useMemo(
() => state.activeIds.filter(id => allowedIds.has(id)),
[state.activeIds, allowedIds],
)
useEffect(() => {
setState(loadStoredLayoutState())
}, [])
@@ -486,13 +568,13 @@ export function DashboardClient() {
const fetchData = useCallback(async () => {
setLoading(true)
try {
setData(await loadDashboardData())
setData(await loadDashboardData(new Set(permittedActiveIds)))
} catch (err) {
toast.add({ title: 'Failed to load dashboard', description: getApiError(err), type: 'error' })
} finally {
setLoading(false)
}
}, [])
}, [permittedActiveIds])
useEffect(() => {
fetchData()
@@ -525,11 +607,11 @@ export function DashboardClient() {
}
const visibleWidgets = useMemo(
() => state.activeIds
() => permittedActiveIds
.map(id => WIDGET_META.find(w => w.id === id))
.filter((w): w is WidgetMeta => Boolean(w))
.filter(w => category === 'all' || w.cat === category),
[state.activeIds, category],
[permittedActiveIds, category],
)
const visibleLayouts = useMemo(() => {
@@ -577,7 +659,7 @@ export function DashboardClient() {
</button>
</div>
<div className="flex flex-col gap-1">
{WIDGET_META.map(w => {
{WIDGET_META.filter(w => allowedIds.has(w.id)).map(w => {
const checked = state.activeIds.includes(w.id)
return (
<label