mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Load dashboard layout, data and translations from json
This commit is contained in:
247
DASHBOARD_CONFIG.md
Normal file
247
DASHBOARD_CONFIG.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Dashboard JSON Configuration
|
||||
|
||||
The dashboard layout, data, and translations are now loaded from JSON files, making it easy to customize and configure the dashboard without modifying React components.
|
||||
|
||||
## Files
|
||||
|
||||
- **`/src/data/dashboard.json`** - Dashboard layout and configuration
|
||||
- **`/src/data/translations/en.json`** - English translations (including dashboard keys)
|
||||
- **`/src/data/translations/fr.json`** - French translations
|
||||
- **`/src/data/translations/es.json`** - Spanish translations
|
||||
|
||||
## Dashboard Configuration Structure
|
||||
|
||||
### Layout Sections
|
||||
|
||||
The dashboard is divided into three main sections:
|
||||
|
||||
#### 1. Metrics Grid
|
||||
Displays key performance indicators (KPIs) in a grid layout.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "metrics",
|
||||
"type": "metrics-grid",
|
||||
"columns": {
|
||||
"mobile": 1,
|
||||
"tablet": 2,
|
||||
"desktop": 4
|
||||
},
|
||||
"metrics": [
|
||||
{
|
||||
"id": "pendingApprovals",
|
||||
"titleKey": "dashboard.pendingApprovals",
|
||||
"dataSource": "metrics.pendingApprovals",
|
||||
"icon": "ClockCounterClockwise",
|
||||
"iconColor": "text-warning",
|
||||
"variant": "warning",
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "up",
|
||||
"value": 12,
|
||||
"textKey": "dashboard.vsLastWeek",
|
||||
"textParams": { "value": "12" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Financial Summary Cards
|
||||
Shows financial metrics like revenue, payroll, and margins.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "financial-summary",
|
||||
"type": "cards-grid",
|
||||
"columns": {
|
||||
"mobile": 1,
|
||||
"tablet": 1,
|
||||
"desktop": 3
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"id": "monthlyRevenue",
|
||||
"titleKey": "dashboard.monthlyRevenue",
|
||||
"descriptionKey": "dashboard.monthlyRevenueDescription",
|
||||
"dataSource": "metrics.monthlyRevenue",
|
||||
"format": "currency",
|
||||
"currencySymbol": "£",
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "up",
|
||||
"value": 12.5,
|
||||
"textKey": "dashboard.vsLastMonth",
|
||||
"textParams": { "value": "12.5" },
|
||||
"color": "text-success"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Activity Feed & Quick Actions
|
||||
Two-column layout showing recent activities and quick action buttons.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "activity-and-actions",
|
||||
"type": "two-column-cards",
|
||||
"columns": {
|
||||
"mobile": 1,
|
||||
"tablet": 1,
|
||||
"desktop": 2
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"id": "recentActivity",
|
||||
"type": "activity-feed",
|
||||
"titleKey": "dashboard.recentActivity",
|
||||
"descriptionKey": "dashboard.recentActivityDescription",
|
||||
"dataSource": "recentActivities",
|
||||
"maxItems": 4
|
||||
},
|
||||
{
|
||||
"id": "quickActions",
|
||||
"type": "action-list",
|
||||
"titleKey": "dashboard.quickActions",
|
||||
"descriptionKey": "dashboard.quickActionsDescription",
|
||||
"actions": [
|
||||
{
|
||||
"id": "createTimesheet",
|
||||
"labelKey": "dashboard.createTimesheet",
|
||||
"icon": "Clock",
|
||||
"action": "navigate",
|
||||
"target": "timesheets"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Recent Activities
|
||||
|
||||
Activity feed items are defined separately:
|
||||
|
||||
```json
|
||||
{
|
||||
"recentActivities": [
|
||||
{
|
||||
"id": "activity-1",
|
||||
"icon": "CheckCircle",
|
||||
"iconColor": "text-success",
|
||||
"titleKey": "dashboard.timesheetApproved",
|
||||
"description": "John Smith - Week ending 15 Jan 2025",
|
||||
"timeKey": "dashboard.minutesAgo",
|
||||
"timeParams": { "value": "5" },
|
||||
"timestamp": "2025-01-15T14:55:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Data Sources
|
||||
|
||||
The `dataSource` field references metrics from the application's state:
|
||||
|
||||
- `metrics.pendingApprovals` → Dashboard metrics object
|
||||
- `metrics.monthlyRevenue` → Financial data
|
||||
- `metrics.complianceAlerts` → Compliance tracking
|
||||
|
||||
The hook automatically resolves nested paths like `metrics.pendingApprovals` by traversing the metrics object.
|
||||
|
||||
## Available Icons
|
||||
|
||||
Icons are mapped from Phosphor Icons library:
|
||||
- Clock
|
||||
- Receipt
|
||||
- CurrencyDollar
|
||||
- ClockCounterClockwise
|
||||
- CheckCircle
|
||||
- Warning
|
||||
- Notepad
|
||||
- Download
|
||||
- ArrowUp
|
||||
- ArrowDown
|
||||
|
||||
## Variants
|
||||
|
||||
Metric cards support visual variants:
|
||||
- `default` - Standard border
|
||||
- `success` - Green accent border
|
||||
- `warning` - Yellow/orange accent border
|
||||
- `error` - Red accent border
|
||||
|
||||
## Formats
|
||||
|
||||
Financial cards support multiple formats:
|
||||
- `currency` - Displays with currency symbol (e.g., £1,234)
|
||||
- `percentage` - Displays with % symbol (e.g., 15.5%)
|
||||
- `number` - Plain number with locale formatting
|
||||
|
||||
## Translations
|
||||
|
||||
All text is loaded from translation files using translation keys:
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"subtitle": "Real-time overview of your workforce operations",
|
||||
"pendingApprovals": "Pending Approvals",
|
||||
"monthlyRevenue": "Monthly Revenue",
|
||||
"vsLastWeek": "{{value}}% vs last week",
|
||||
"minutesAgo": "{{value}} minutes ago"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Translation keys support parameter interpolation using `{{paramName}}` syntax.
|
||||
|
||||
## Custom Hook
|
||||
|
||||
The `useDashboardConfig` hook provides easy access to the configuration:
|
||||
|
||||
```typescript
|
||||
import { useDashboardConfig } from '@/hooks/use-dashboard-config'
|
||||
|
||||
const {
|
||||
config, // Full configuration object
|
||||
loading, // Loading state
|
||||
error, // Error state
|
||||
getMetricsSection, // Get metrics section config
|
||||
getFinancialSection, // Get financial cards config
|
||||
getRecentActivities, // Get activity feed (with optional limit)
|
||||
getQuickActions // Get quick action buttons
|
||||
} = useDashboardConfig()
|
||||
```
|
||||
|
||||
## Adding New Metrics
|
||||
|
||||
To add a new metric:
|
||||
|
||||
1. Add the metric to `/src/data/dashboard.json` in the appropriate section
|
||||
2. Add translation keys to all language files (`en.json`, `fr.json`, `es.json`)
|
||||
3. Ensure the data source path matches your metrics object structure
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"id": "activeWorkers",
|
||||
"titleKey": "dashboard.activeWorkers",
|
||||
"dataSource": "metrics.activeWorkers",
|
||||
"icon": "Users",
|
||||
"iconColor": "text-info",
|
||||
"variant": "default"
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
✅ **No code changes needed** - Update dashboard by editing JSON
|
||||
✅ **Fully internationalized** - All text comes from translation files
|
||||
✅ **Flexible layout** - Responsive column configurations
|
||||
✅ **Type-safe** - TypeScript interfaces ensure correct structure
|
||||
✅ **Easy maintenance** - Centralized configuration
|
||||
✅ **Reusable** - Same pattern can be applied to other views
|
||||
@@ -15,13 +15,59 @@ import {
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { DashboardMetrics } from '@/lib/types'
|
||||
import { useTranslation } from '@/hooks/use-translation'
|
||||
import { useDashboardConfig, type DashboardMetric, type DashboardCard, type DashboardActivity, type DashboardAction } from '@/hooks/use-dashboard-config'
|
||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||
|
||||
interface DashboardViewProps {
|
||||
metrics: DashboardMetrics
|
||||
}
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<any>> = {
|
||||
Clock,
|
||||
Receipt,
|
||||
CurrencyDollar,
|
||||
ClockCounterClockwise,
|
||||
CheckCircle,
|
||||
Warning,
|
||||
Notepad,
|
||||
Download,
|
||||
ArrowUp,
|
||||
ArrowDown
|
||||
}
|
||||
|
||||
export function DashboardView({ metrics }: DashboardViewProps) {
|
||||
const { t } = useTranslation()
|
||||
const { config, loading, error, getMetricsSection, getFinancialSection, getRecentActivities, getQuickActions } = useDashboardConfig()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !config) {
|
||||
return (
|
||||
<div className="text-center text-destructive py-8">
|
||||
Failed to load dashboard configuration
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const metricsSection = getMetricsSection()
|
||||
const financialSection = getFinancialSection()
|
||||
const activities = getRecentActivities(4)
|
||||
const actions = getQuickActions()
|
||||
|
||||
const getMetricValue = (dataSource: string): number => {
|
||||
const path = dataSource.split('.')
|
||||
let value: any = { metrics }
|
||||
for (const key of path) {
|
||||
value = value?.[key]
|
||||
}
|
||||
return typeof value === 'number' ? value : 0
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -30,86 +76,39 @@ export function DashboardView({ metrics }: DashboardViewProps) {
|
||||
<p className="text-muted-foreground mt-1">{t('dashboard.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
title={t('dashboard.pendingApprovals')}
|
||||
value={metrics.pendingApprovals}
|
||||
icon={<ClockCounterClockwise size={20} className="text-warning" />}
|
||||
trend={{ value: 12, direction: 'up' }}
|
||||
trendText={t('dashboard.vsLastWeek', { value: '12' })}
|
||||
variant="warning"
|
||||
/>
|
||||
<MetricCard
|
||||
title={t('dashboard.pendingExpenses')}
|
||||
value={metrics.pendingExpenses}
|
||||
icon={<Notepad size={20} className="text-info" />}
|
||||
variant="default"
|
||||
/>
|
||||
<MetricCard
|
||||
title={t('dashboard.overdueInvoices')}
|
||||
value={metrics.overdueInvoices}
|
||||
icon={<Receipt size={20} className="text-destructive" />}
|
||||
trend={{ value: 5, direction: 'down' }}
|
||||
trendText={t('dashboard.vsLastWeek', { value: '5' })}
|
||||
variant="error"
|
||||
/>
|
||||
<MetricCard
|
||||
title={t('dashboard.complianceAlerts')}
|
||||
value={metrics.complianceAlerts}
|
||||
icon={<Warning size={20} className="text-warning" />}
|
||||
variant="warning"
|
||||
/>
|
||||
</div>
|
||||
{metricsSection && (
|
||||
<div className={cn(
|
||||
'grid gap-4',
|
||||
`grid-cols-${metricsSection.columns.mobile}`,
|
||||
`md:grid-cols-${metricsSection.columns.tablet}`,
|
||||
`lg:grid-cols-${metricsSection.columns.desktop}`
|
||||
)}>
|
||||
{metricsSection.metrics?.map((metric) => (
|
||||
<MetricCardFromConfig
|
||||
key={metric.id}
|
||||
metric={metric}
|
||||
value={getMetricValue(metric.dataSource)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('dashboard.monthlyRevenue')}</CardTitle>
|
||||
<CardDescription>{t('dashboard.monthlyRevenueDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold font-mono">
|
||||
£{(metrics.monthlyRevenue || 0).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-2 text-sm text-success">
|
||||
<ArrowUp size={16} weight="bold" />
|
||||
<span>{t('dashboard.vsLastMonth', { value: '12.5' })}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('dashboard.monthlyPayroll')}</CardTitle>
|
||||
<CardDescription>{t('dashboard.monthlyPayrollDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold font-mono">
|
||||
£{(metrics.monthlyPayroll || 0).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-2 text-sm text-muted-foreground">
|
||||
<ArrowUp size={16} weight="bold" />
|
||||
<span>{t('dashboard.vsLastMonth', { value: '8.3' })}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('dashboard.grossMargin')}</CardTitle>
|
||||
<CardDescription>{t('dashboard.grossMarginDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold font-mono">
|
||||
{(metrics.grossMargin || 0).toFixed(1)}%
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-2 text-sm text-success">
|
||||
<ArrowUp size={16} weight="bold" />
|
||||
<span>{t('dashboard.vsLastMonth', { value: '3.2' })}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{financialSection && (
|
||||
<div className={cn(
|
||||
'grid gap-4',
|
||||
`grid-cols-${financialSection.columns.mobile}`,
|
||||
`md:grid-cols-${financialSection.columns.tablet}`,
|
||||
`lg:grid-cols-${financialSection.columns.desktop}`
|
||||
)}>
|
||||
{financialSection.cards?.map((card) => (
|
||||
<FinancialCardFromConfig
|
||||
key={card.id}
|
||||
card={card}
|
||||
value={getMetricValue(card.dataSource || '')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
@@ -119,30 +118,9 @@ export function DashboardView({ metrics }: DashboardViewProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<ActivityItem
|
||||
icon={<CheckCircle size={18} className="text-success" />}
|
||||
title={t('dashboard.timesheetApproved')}
|
||||
description="John Smith - Week ending 15 Jan 2025"
|
||||
time={t('dashboard.minutesAgo', { value: '5' })}
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<Receipt size={18} className="text-info" />}
|
||||
title={t('dashboard.invoiceGenerated')}
|
||||
description="INV-00234 - Acme Corp - £2,450"
|
||||
time={t('dashboard.minutesAgo', { value: '12' })}
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<CheckCircle size={18} className="text-success" />}
|
||||
title={t('dashboard.payrollCompleted')}
|
||||
description="Weekly run - 45 workers - £28,900"
|
||||
time={t('dashboard.hourAgo', { value: '1' })}
|
||||
/>
|
||||
<ActivityItem
|
||||
icon={<Warning size={18} className="text-warning" />}
|
||||
title={t('dashboard.documentExpiringSoon')}
|
||||
description={`DBS check for Sarah Johnson - ${t('dashboard.days', { value: '14' })}`}
|
||||
time={t('dashboard.hoursAgo', { value: '2' })}
|
||||
/>
|
||||
{activities.map((activity) => (
|
||||
<ActivityItemFromConfig key={activity.id} activity={activity} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -153,22 +131,9 @@ export function DashboardView({ metrics }: DashboardViewProps) {
|
||||
<CardDescription>{t('dashboard.quickActionsDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
<Clock size={18} className="mr-2" />
|
||||
{t('dashboard.createTimesheet')}
|
||||
</Button>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
<Receipt size={18} className="mr-2" />
|
||||
{t('dashboard.generateInvoice')}
|
||||
</Button>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
<CurrencyDollar size={18} className="mr-2" />
|
||||
{t('dashboard.runPayroll')}
|
||||
</Button>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
<Download size={18} className="mr-2" />
|
||||
{t('dashboard.exportReports')}
|
||||
</Button>
|
||||
{actions.map((action) => (
|
||||
<QuickActionFromConfig key={action.id} action={action} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -176,16 +141,15 @@ export function DashboardView({ metrics }: DashboardViewProps) {
|
||||
)
|
||||
}
|
||||
|
||||
interface MetricCardProps {
|
||||
title: string
|
||||
interface MetricCardFromConfigProps {
|
||||
metric: DashboardMetric
|
||||
value: number
|
||||
icon: React.ReactNode
|
||||
trend?: { value: number; direction: 'up' | 'down' }
|
||||
trendText?: string
|
||||
variant?: 'default' | 'success' | 'warning' | 'error'
|
||||
}
|
||||
|
||||
function MetricCard({ title, value, icon, trend, trendText, variant = 'default' }: MetricCardProps) {
|
||||
function MetricCardFromConfig({ metric, value }: MetricCardFromConfigProps) {
|
||||
const { t } = useTranslation()
|
||||
const IconComponent = iconMap[metric.icon]
|
||||
|
||||
const borderColors = {
|
||||
default: 'border-border',
|
||||
success: 'border-success/20',
|
||||
@@ -194,26 +158,26 @@ function MetricCard({ title, value, icon, trend, trendText, variant = 'default'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={cn('border-l-4', borderColors[variant])}>
|
||||
<Card className={cn('border-l-4', borderColors[metric.variant])}>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{title}
|
||||
{t(metric.titleKey)}
|
||||
</CardTitle>
|
||||
{icon}
|
||||
{IconComponent && <IconComponent size={20} className={metric.iconColor} />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">{value}</div>
|
||||
{trend && (
|
||||
{metric.trend?.enabled && (
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 mt-1 text-xs',
|
||||
trend.direction === 'up' ? 'text-success' : 'text-muted-foreground'
|
||||
metric.trend.direction === 'up' ? 'text-success' : 'text-muted-foreground'
|
||||
)}>
|
||||
{trend.direction === 'up' ? (
|
||||
{metric.trend.direction === 'up' ? (
|
||||
<ArrowUp size={14} weight="bold" />
|
||||
) : (
|
||||
<ArrowDown size={14} weight="bold" />
|
||||
)}
|
||||
<span>{trendText || `${trend.value}% vs last week`}</span>
|
||||
<span>{t(metric.trend.textKey, metric.trend.textParams)}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -221,22 +185,87 @@ function MetricCard({ title, value, icon, trend, trendText, variant = 'default'
|
||||
)
|
||||
}
|
||||
|
||||
interface ActivityItemProps {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
time: string
|
||||
interface FinancialCardFromConfigProps {
|
||||
card: DashboardCard
|
||||
value: number
|
||||
}
|
||||
|
||||
function ActivityItem({ icon, title, description, time }: ActivityItemProps) {
|
||||
function FinancialCardFromConfig({ card, value }: FinancialCardFromConfigProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const formatValue = () => {
|
||||
if (card.format === 'currency') {
|
||||
return `${card.currencySymbol || ''}${value.toLocaleString()}`
|
||||
} else if (card.format === 'percentage') {
|
||||
return `${value.toFixed(card.decimals || 0)}%`
|
||||
}
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t(card.titleKey)}</CardTitle>
|
||||
{card.descriptionKey && <CardDescription>{t(card.descriptionKey)}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold font-mono">
|
||||
{formatValue()}
|
||||
</div>
|
||||
{card.trend?.enabled && (
|
||||
<div className={cn('flex items-center gap-1 mt-2 text-sm', card.trend.color)}>
|
||||
{card.trend.direction === 'up' ? (
|
||||
<ArrowUp size={16} weight="bold" />
|
||||
) : (
|
||||
<ArrowDown size={16} weight="bold" />
|
||||
)}
|
||||
<span>{t(card.trend.textKey, card.trend.textParams)}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
interface ActivityItemFromConfigProps {
|
||||
activity: DashboardActivity
|
||||
}
|
||||
|
||||
function ActivityItemFromConfig({ activity }: ActivityItemFromConfigProps) {
|
||||
const { t } = useTranslation()
|
||||
const IconComponent = iconMap[activity.icon]
|
||||
|
||||
const description = activity.description ||
|
||||
(activity.descriptionKey ? t(activity.descriptionKey, activity.descriptionParams) : '')
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">{icon}</div>
|
||||
<div className="mt-0.5">
|
||||
{IconComponent && <IconComponent size={18} className={activity.iconColor} />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">{title}</p>
|
||||
<p className="text-sm font-medium">{t(activity.titleKey)}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{description}</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">{time}</span>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{t(activity.timeKey, activity.timeParams)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface QuickActionFromConfigProps {
|
||||
action: DashboardAction
|
||||
}
|
||||
|
||||
function QuickActionFromConfig({ action }: QuickActionFromConfigProps) {
|
||||
const { t } = useTranslation()
|
||||
const IconComponent = iconMap[action.icon]
|
||||
|
||||
return (
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
{IconComponent && <IconComponent size={18} className="mr-2" />}
|
||||
{t(action.labelKey)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
220
src/data/dashboard.json
Normal file
220
src/data/dashboard.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"layout": {
|
||||
"sections": [
|
||||
{
|
||||
"id": "metrics",
|
||||
"type": "metrics-grid",
|
||||
"columns": {
|
||||
"mobile": 1,
|
||||
"tablet": 2,
|
||||
"desktop": 4
|
||||
},
|
||||
"metrics": [
|
||||
{
|
||||
"id": "pendingApprovals",
|
||||
"titleKey": "dashboard.pendingApprovals",
|
||||
"dataSource": "metrics.pendingApprovals",
|
||||
"icon": "ClockCounterClockwise",
|
||||
"iconColor": "text-warning",
|
||||
"variant": "warning",
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "up",
|
||||
"value": 12,
|
||||
"textKey": "dashboard.vsLastWeek",
|
||||
"textParams": { "value": "12" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pendingExpenses",
|
||||
"titleKey": "dashboard.pendingExpenses",
|
||||
"dataSource": "metrics.pendingExpenses",
|
||||
"icon": "Notepad",
|
||||
"iconColor": "text-info",
|
||||
"variant": "default"
|
||||
},
|
||||
{
|
||||
"id": "overdueInvoices",
|
||||
"titleKey": "dashboard.overdueInvoices",
|
||||
"dataSource": "metrics.overdueInvoices",
|
||||
"icon": "Receipt",
|
||||
"iconColor": "text-destructive",
|
||||
"variant": "error",
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "down",
|
||||
"value": 5,
|
||||
"textKey": "dashboard.vsLastWeek",
|
||||
"textParams": { "value": "5" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "complianceAlerts",
|
||||
"titleKey": "dashboard.complianceAlerts",
|
||||
"dataSource": "metrics.complianceAlerts",
|
||||
"icon": "Warning",
|
||||
"iconColor": "text-warning",
|
||||
"variant": "warning"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "financial-summary",
|
||||
"type": "cards-grid",
|
||||
"columns": {
|
||||
"mobile": 1,
|
||||
"tablet": 1,
|
||||
"desktop": 3
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"id": "monthlyRevenue",
|
||||
"titleKey": "dashboard.monthlyRevenue",
|
||||
"descriptionKey": "dashboard.monthlyRevenueDescription",
|
||||
"dataSource": "metrics.monthlyRevenue",
|
||||
"format": "currency",
|
||||
"currencySymbol": "£",
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "up",
|
||||
"value": 12.5,
|
||||
"textKey": "dashboard.vsLastMonth",
|
||||
"textParams": { "value": "12.5" },
|
||||
"color": "text-success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "monthlyPayroll",
|
||||
"titleKey": "dashboard.monthlyPayroll",
|
||||
"descriptionKey": "dashboard.monthlyPayrollDescription",
|
||||
"dataSource": "metrics.monthlyPayroll",
|
||||
"format": "currency",
|
||||
"currencySymbol": "£",
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "up",
|
||||
"value": 8.3,
|
||||
"textKey": "dashboard.vsLastMonth",
|
||||
"textParams": { "value": "8.3" },
|
||||
"color": "text-muted-foreground"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "grossMargin",
|
||||
"titleKey": "dashboard.grossMargin",
|
||||
"descriptionKey": "dashboard.grossMarginDescription",
|
||||
"dataSource": "metrics.grossMargin",
|
||||
"format": "percentage",
|
||||
"decimals": 1,
|
||||
"trend": {
|
||||
"enabled": true,
|
||||
"direction": "up",
|
||||
"value": 3.2,
|
||||
"textKey": "dashboard.vsLastMonth",
|
||||
"textParams": { "value": "3.2" },
|
||||
"color": "text-success"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "activity-and-actions",
|
||||
"type": "two-column-cards",
|
||||
"columns": {
|
||||
"mobile": 1,
|
||||
"tablet": 1,
|
||||
"desktop": 2
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"id": "recentActivity",
|
||||
"type": "activity-feed",
|
||||
"titleKey": "dashboard.recentActivity",
|
||||
"descriptionKey": "dashboard.recentActivityDescription",
|
||||
"dataSource": "recentActivities",
|
||||
"maxItems": 4
|
||||
},
|
||||
{
|
||||
"id": "quickActions",
|
||||
"type": "action-list",
|
||||
"titleKey": "dashboard.quickActions",
|
||||
"descriptionKey": "dashboard.quickActionsDescription",
|
||||
"actions": [
|
||||
{
|
||||
"id": "createTimesheet",
|
||||
"labelKey": "dashboard.createTimesheet",
|
||||
"icon": "Clock",
|
||||
"action": "navigate",
|
||||
"target": "timesheets"
|
||||
},
|
||||
{
|
||||
"id": "generateInvoice",
|
||||
"labelKey": "dashboard.generateInvoice",
|
||||
"icon": "Receipt",
|
||||
"action": "navigate",
|
||||
"target": "billing"
|
||||
},
|
||||
{
|
||||
"id": "runPayroll",
|
||||
"labelKey": "dashboard.runPayroll",
|
||||
"icon": "CurrencyDollar",
|
||||
"action": "navigate",
|
||||
"target": "payroll"
|
||||
},
|
||||
{
|
||||
"id": "exportReports",
|
||||
"labelKey": "dashboard.exportReports",
|
||||
"icon": "Download",
|
||||
"action": "navigate",
|
||||
"target": "reports"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"recentActivities": [
|
||||
{
|
||||
"id": "activity-1",
|
||||
"icon": "CheckCircle",
|
||||
"iconColor": "text-success",
|
||||
"titleKey": "dashboard.timesheetApproved",
|
||||
"description": "John Smith - Week ending 15 Jan 2025",
|
||||
"timeKey": "dashboard.minutesAgo",
|
||||
"timeParams": { "value": "5" },
|
||||
"timestamp": "2025-01-15T14:55:00Z"
|
||||
},
|
||||
{
|
||||
"id": "activity-2",
|
||||
"icon": "Receipt",
|
||||
"iconColor": "text-info",
|
||||
"titleKey": "dashboard.invoiceGenerated",
|
||||
"description": "INV-00234 - Acme Corp - £2,450",
|
||||
"timeKey": "dashboard.minutesAgo",
|
||||
"timeParams": { "value": "12" },
|
||||
"timestamp": "2025-01-15T14:48:00Z"
|
||||
},
|
||||
{
|
||||
"id": "activity-3",
|
||||
"icon": "CheckCircle",
|
||||
"iconColor": "text-success",
|
||||
"titleKey": "dashboard.payrollCompleted",
|
||||
"description": "Weekly run - 45 workers - £28,900",
|
||||
"timeKey": "dashboard.hourAgo",
|
||||
"timeParams": { "value": "1" },
|
||||
"timestamp": "2025-01-15T13:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "activity-4",
|
||||
"icon": "Warning",
|
||||
"iconColor": "text-warning",
|
||||
"titleKey": "dashboard.documentExpiringSoon",
|
||||
"descriptionKey": "dashboard.documentExpiringSoonDescription",
|
||||
"descriptionParams": { "name": "Sarah Johnson", "days": "14" },
|
||||
"timeKey": "dashboard.hoursAgo",
|
||||
"timeParams": { "value": "2" },
|
||||
"timestamp": "2025-01-15T12:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -106,6 +106,7 @@
|
||||
"invoiceGenerated": "Invoice generated",
|
||||
"payrollCompleted": "Payroll completed",
|
||||
"documentExpiringSoon": "Document expiring soon",
|
||||
"documentExpiringSoonDescription": "DBS check for {{name}} - {{days}} days",
|
||||
"vsLastWeek": "{{value}}% vs last week",
|
||||
"vsLastMonth": "{{value}}% from last month",
|
||||
"minutesAgo": "{{value}} minutes ago",
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"invoiceGenerated": "Factura generada",
|
||||
"payrollCompleted": "Nómina completada",
|
||||
"documentExpiringSoon": "Documento por vencer pronto",
|
||||
"documentExpiringSoonDescription": "Verificación DBS para {{name}} - {{days}} días",
|
||||
"vsLastWeek": "{{value}}% vs semana pasada",
|
||||
"vsLastMonth": "{{value}}% desde el mes pasado",
|
||||
"minutesAgo": "hace {{value}} minutos",
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"invoiceGenerated": "Facture générée",
|
||||
"payrollCompleted": "Paie terminée",
|
||||
"documentExpiringSoon": "Document expirant bientôt",
|
||||
"documentExpiringSoonDescription": "Vérification DBS pour {{name}} - {{days}} jours",
|
||||
"vsLastWeek": "{{value}}% par rapport à la semaine dernière",
|
||||
"vsLastMonth": "{{value}}% par rapport au mois dernier",
|
||||
"minutesAgo": "il y a {{value}} minutes",
|
||||
|
||||
@@ -72,6 +72,7 @@ export { useFilterableData } from './use-filterable-data'
|
||||
export { useFormatter } from './use-formatter'
|
||||
export { useTemplateManager } from './use-template-manager'
|
||||
export { useLocaleInit } from './use-locale-init'
|
||||
export { useDashboardConfig } from './use-dashboard-config'
|
||||
|
||||
export { useFetch } from './use-fetch'
|
||||
export { useLocalStorageState } from './use-local-storage-state'
|
||||
|
||||
131
src/hooks/use-dashboard-config.ts
Normal file
131
src/hooks/use-dashboard-config.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import dashboardConfig from '@/data/dashboard.json'
|
||||
|
||||
export interface DashboardMetric {
|
||||
id: string
|
||||
titleKey: string
|
||||
dataSource: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
variant: 'default' | 'success' | 'warning' | 'error'
|
||||
trend?: {
|
||||
enabled: boolean
|
||||
direction: 'up' | 'down'
|
||||
value: number
|
||||
textKey: string
|
||||
textParams?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
export interface DashboardCard {
|
||||
id: string
|
||||
type?: string
|
||||
titleKey: string
|
||||
descriptionKey?: string
|
||||
dataSource?: string
|
||||
format?: 'currency' | 'percentage' | 'number'
|
||||
currencySymbol?: string
|
||||
decimals?: number
|
||||
trend?: {
|
||||
enabled: boolean
|
||||
direction: 'up' | 'down'
|
||||
value: number
|
||||
textKey: string
|
||||
textParams?: Record<string, string>
|
||||
color: string
|
||||
}
|
||||
actions?: DashboardAction[]
|
||||
}
|
||||
|
||||
export interface DashboardActivity {
|
||||
id: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
titleKey: string
|
||||
description?: string
|
||||
descriptionKey?: string
|
||||
descriptionParams?: Record<string, string>
|
||||
timeKey: string
|
||||
timeParams?: Record<string, string>
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface DashboardAction {
|
||||
id: string
|
||||
labelKey: string
|
||||
icon: string
|
||||
action: string
|
||||
target: string
|
||||
}
|
||||
|
||||
export interface DashboardSection {
|
||||
id: string
|
||||
type: string
|
||||
columns: {
|
||||
mobile: number
|
||||
tablet: number
|
||||
desktop: number
|
||||
}
|
||||
metrics?: DashboardMetric[]
|
||||
cards?: DashboardCard[]
|
||||
actions?: DashboardAction[]
|
||||
}
|
||||
|
||||
export interface DashboardConfig {
|
||||
layout: {
|
||||
sections: DashboardSection[]
|
||||
}
|
||||
recentActivities: DashboardActivity[]
|
||||
}
|
||||
|
||||
export function useDashboardConfig() {
|
||||
const [config, setConfig] = useState<DashboardConfig | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
setConfig(dashboardConfig as DashboardConfig)
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to load dashboard config'))
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getMetricsSection = () => {
|
||||
return config?.layout.sections.find(s => s.type === 'metrics-grid')
|
||||
}
|
||||
|
||||
const getFinancialSection = () => {
|
||||
return config?.layout.sections.find(s => s.type === 'cards-grid')
|
||||
}
|
||||
|
||||
const getActivitySection = () => {
|
||||
return config?.layout.sections.find(s => s.type === 'two-column-cards')
|
||||
}
|
||||
|
||||
const getRecentActivities = (maxItems?: number) => {
|
||||
if (!config?.recentActivities) return []
|
||||
return maxItems
|
||||
? config.recentActivities.slice(0, maxItems)
|
||||
: config.recentActivities
|
||||
}
|
||||
|
||||
const getQuickActions = () => {
|
||||
const activitySection = getActivitySection()
|
||||
const actionsCard = activitySection?.cards?.find(c => c.type === 'action-list')
|
||||
return actionsCard?.actions || []
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
loading,
|
||||
error,
|
||||
getMetricsSection,
|
||||
getFinancialSection,
|
||||
getActivitySection,
|
||||
getRecentActivities,
|
||||
getQuickActions
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user