feat(dashboard): implement customizable dashboard with widget support
- Added `react-grid-layout` for drag-and-drop functionality. - Created `DashboardCanvas` component to manage layout and widget interactions. - Introduced `WidgetContainer` and `WidgetPicker` components for widget management. - Implemented various widgets: Active Rides, Today Sales, Pending Purchase Orders, Stock Alerts, Recent Orders, and Loading Sheets. - Integrated API calls for fetching and saving dashboard preferences. - Defined default layouts for different user roles. - Created a card component for consistent widget styling.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export interface WidgetLayoutItem {
|
||||
i: string
|
||||
widgetType: string
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export interface DashboardPreferencesResponse {
|
||||
userId: string | null
|
||||
role: string
|
||||
isDefault: boolean
|
||||
layout: WidgetLayoutItem[]
|
||||
}
|
||||
|
||||
export const dashboard = {
|
||||
getPreferences: () =>
|
||||
api.get<DashboardPreferencesResponse>('/dashboard/preferences').then(r => r.data),
|
||||
|
||||
savePreferences: (layout: WidgetLayoutItem[]) =>
|
||||
api.put<DashboardPreferencesResponse>('/dashboard/preferences', { layout }).then(r => r.data),
|
||||
|
||||
getDefaults: (role: string) =>
|
||||
api.get<DashboardPreferencesResponse>(`/dashboard/defaults/${role}`).then(r => r.data),
|
||||
|
||||
saveDefaults: (role: string, layout: WidgetLayoutItem[]) =>
|
||||
api.put<DashboardPreferencesResponse>(`/dashboard/defaults/${role}`, { layout }).then(r => r.data),
|
||||
}
|
||||
@@ -10,6 +10,8 @@ export { rides, loadingSheets, deliveryTasks } from './operations'
|
||||
export { stock } from './stock'
|
||||
export { sales, orders, payments } from './sales'
|
||||
export { getPaginatedItems } from './paginated'
|
||||
export { dashboard } from './dashboard'
|
||||
export type { WidgetLayoutItem, DashboardPreferencesResponse } from './dashboard'
|
||||
|
||||
export type { PageParams } from './master'
|
||||
export { getApiError } from '@/lib/api'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { WidgetLayoutItem } from '@/lib/api/dashboard'
|
||||
|
||||
export const ROLE_DEFAULT_LAYOUTS: Record<string, WidgetLayoutItem[]> = {
|
||||
admin: [
|
||||
{ i: 'today-sales-1', widgetType: 'today-sales', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: 'active-rides-1', widgetType: 'active-rides', x: 4, y: 0, w: 4, h: 4 },
|
||||
{ i: 'stock-alerts-1', widgetType: 'stock-alerts', x: 8, y: 0, w: 4, h: 4 },
|
||||
{ i: 'pending-purchase-orders-1', widgetType: 'pending-purchase-orders', x: 0, y: 4, w: 4, h: 4 },
|
||||
{ i: 'loading-sheets-1', widgetType: 'loading-sheets', x: 4, y: 4, w: 4, h: 4 },
|
||||
{ i: 'recent-orders-1', widgetType: 'recent-orders', x: 0, y: 8, w: 8, h: 4 },
|
||||
],
|
||||
warehouse: [
|
||||
{ i: 'active-rides-1', widgetType: 'active-rides', x: 0, y: 0, w: 6, h: 4 },
|
||||
{ i: 'stock-alerts-1', widgetType: 'stock-alerts', x: 6, y: 0, w: 6, h: 4 },
|
||||
{ i: 'loading-sheets-1', widgetType: 'loading-sheets', x: 0, y: 4, w: 6, h: 4 },
|
||||
{ i: 'pending-purchase-orders-1', widgetType: 'pending-purchase-orders', x: 6, y: 4, w: 6, h: 4 },
|
||||
],
|
||||
sales: [
|
||||
{ i: 'today-sales-1', widgetType: 'today-sales', x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: 'recent-orders-1', widgetType: 'recent-orders', x: 4, y: 0, w: 8, h: 4 },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { Role } from '@/contexts/auth-context'
|
||||
|
||||
import { ActiveRidesWidget } from '@/components/dashboard/widgets/active-rides-widget'
|
||||
import { TodaySalesWidget } from '@/components/dashboard/widgets/today-sales-widget'
|
||||
import { PendingPOsWidget } from '@/components/dashboard/widgets/pending-pos-widget'
|
||||
import { StockAlertsWidget } from '@/components/dashboard/widgets/stock-alerts-widget'
|
||||
import { RecentOrdersWidget } from '@/components/dashboard/widgets/recent-orders-widget'
|
||||
import { LoadingSheetsWidget } from '@/components/dashboard/widgets/loading-sheets-widget'
|
||||
|
||||
export interface WidgetDefinition {
|
||||
type: string
|
||||
label: string
|
||||
description: string
|
||||
defaultW: number
|
||||
defaultH: number
|
||||
minW: number
|
||||
minH: number
|
||||
maxW?: number
|
||||
allowedRoles: Role[] | '*'
|
||||
component: ComponentType
|
||||
}
|
||||
|
||||
export const WIDGET_REGISTRY: Record<string, WidgetDefinition> = {
|
||||
'active-rides': {
|
||||
type: 'active-rides',
|
||||
label: 'Active Rides',
|
||||
description: 'Live view of all currently active rides with driver and route info.',
|
||||
defaultW: 4,
|
||||
defaultH: 4,
|
||||
minW: 3,
|
||||
minH: 3,
|
||||
allowedRoles: ['admin', 'warehouse'],
|
||||
component: ActiveRidesWidget,
|
||||
},
|
||||
'today-sales': {
|
||||
type: 'today-sales',
|
||||
label: "Today's Sales",
|
||||
description: "Total revenue and sales counts for today.",
|
||||
defaultW: 4,
|
||||
defaultH: 4,
|
||||
minW: 2,
|
||||
minH: 3,
|
||||
allowedRoles: ['admin', 'sales'],
|
||||
component: TodaySalesWidget,
|
||||
},
|
||||
'pending-purchase-orders': {
|
||||
type: 'pending-purchase-orders',
|
||||
label: 'Pending Purchase Orders',
|
||||
description: 'Purchase orders that have been issued but not yet fully received.',
|
||||
defaultW: 4,
|
||||
defaultH: 4,
|
||||
minW: 3,
|
||||
minH: 3,
|
||||
allowedRoles: ['admin', 'warehouse'],
|
||||
component: PendingPOsWidget,
|
||||
},
|
||||
'stock-alerts': {
|
||||
type: 'stock-alerts',
|
||||
label: 'Stock Alerts',
|
||||
description: 'Items at zero or critically low stock levels across all locations.',
|
||||
defaultW: 4,
|
||||
defaultH: 4,
|
||||
minW: 3,
|
||||
minH: 3,
|
||||
allowedRoles: ['admin', 'warehouse'],
|
||||
component: StockAlertsWidget,
|
||||
},
|
||||
'recent-orders': {
|
||||
type: 'recent-orders',
|
||||
label: 'Recent Orders',
|
||||
description: 'Latest customer orders with status and value.',
|
||||
defaultW: 5,
|
||||
defaultH: 4,
|
||||
minW: 4,
|
||||
minH: 3,
|
||||
allowedRoles: ['admin', 'sales'],
|
||||
component: RecentOrdersWidget,
|
||||
},
|
||||
'loading-sheets': {
|
||||
type: 'loading-sheets',
|
||||
label: 'Pending Loading Sheets',
|
||||
description: 'Loading sheets awaiting dispatch confirmation.',
|
||||
defaultW: 4,
|
||||
defaultH: 4,
|
||||
minW: 3,
|
||||
minH: 3,
|
||||
allowedRoles: ['admin', 'warehouse'],
|
||||
component: LoadingSheetsWidget,
|
||||
},
|
||||
}
|
||||
|
||||
export function getWidgetsForRole(role: Role): WidgetDefinition[] {
|
||||
return Object.values(WIDGET_REGISTRY).filter(def =>
|
||||
def.allowedRoles === '*' || def.allowedRoles.includes(role)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user