Generated by Spark: Load dashboard layout, data and translations from json

This commit is contained in:
2026-01-27 15:35:14 +00:00
committed by GitHub
parent 3937d58c0e
commit c509af0bc1
8 changed files with 773 additions and 142 deletions

247
DASHBOARD_CONFIG.md Normal file
View 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

View File

@@ -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
View 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"
}
]
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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'

View 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
}
}