diff --git a/DASHBOARD_CONFIG.md b/DASHBOARD_CONFIG.md new file mode 100644 index 0000000..2bb4fbc --- /dev/null +++ b/DASHBOARD_CONFIG.md @@ -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 diff --git a/src/components/dashboard-view.tsx b/src/components/dashboard-view.tsx index 0cca6c3..e3b2157 100644 --- a/src/components/dashboard-view.tsx +++ b/src/components/dashboard-view.tsx @@ -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> = { + 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 ( +
+ +
+ ) + } + + if (error || !config) { + return ( +
+ Failed to load dashboard configuration +
+ ) + } + + 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 (
@@ -30,86 +76,39 @@ export function DashboardView({ metrics }: DashboardViewProps) {

{t('dashboard.subtitle')}

-
- } - trend={{ value: 12, direction: 'up' }} - trendText={t('dashboard.vsLastWeek', { value: '12' })} - variant="warning" - /> - } - variant="default" - /> - } - trend={{ value: 5, direction: 'down' }} - trendText={t('dashboard.vsLastWeek', { value: '5' })} - variant="error" - /> - } - variant="warning" - /> -
+ {metricsSection && ( +
+ {metricsSection.metrics?.map((metric) => ( + + ))} +
+ )} -
- - - {t('dashboard.monthlyRevenue')} - {t('dashboard.monthlyRevenueDescription')} - - -
- £{(metrics.monthlyRevenue || 0).toLocaleString()} -
-
- - {t('dashboard.vsLastMonth', { value: '12.5' })} -
-
-
- - - - {t('dashboard.monthlyPayroll')} - {t('dashboard.monthlyPayrollDescription')} - - -
- £{(metrics.monthlyPayroll || 0).toLocaleString()} -
-
- - {t('dashboard.vsLastMonth', { value: '8.3' })} -
-
-
- - - - {t('dashboard.grossMargin')} - {t('dashboard.grossMarginDescription')} - - -
- {(metrics.grossMargin || 0).toFixed(1)}% -
-
- - {t('dashboard.vsLastMonth', { value: '3.2' })} -
-
-
-
+ {financialSection && ( +
+ {financialSection.cards?.map((card) => ( + + ))} +
+ )}
@@ -119,30 +118,9 @@ export function DashboardView({ metrics }: DashboardViewProps) {
- } - title={t('dashboard.timesheetApproved')} - description="John Smith - Week ending 15 Jan 2025" - time={t('dashboard.minutesAgo', { value: '5' })} - /> - } - title={t('dashboard.invoiceGenerated')} - description="INV-00234 - Acme Corp - £2,450" - time={t('dashboard.minutesAgo', { value: '12' })} - /> - } - title={t('dashboard.payrollCompleted')} - description="Weekly run - 45 workers - £28,900" - time={t('dashboard.hourAgo', { value: '1' })} - /> - } - title={t('dashboard.documentExpiringSoon')} - description={`DBS check for Sarah Johnson - ${t('dashboard.days', { value: '14' })}`} - time={t('dashboard.hoursAgo', { value: '2' })} - /> + {activities.map((activity) => ( + + ))}
@@ -153,22 +131,9 @@ export function DashboardView({ metrics }: DashboardViewProps) { {t('dashboard.quickActionsDescription')} - - - - + {actions.map((action) => ( + + ))}
@@ -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 ( - + - {title} + {t(metric.titleKey)} - {icon} + {IconComponent && }
{value}
- {trend && ( + {metric.trend?.enabled && (
- {trend.direction === 'up' ? ( + {metric.trend.direction === 'up' ? ( ) : ( )} - {trendText || `${trend.value}% vs last week`} + {t(metric.trend.textKey, metric.trend.textParams)}
)}
@@ -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 ( + + + {t(card.titleKey)} + {card.descriptionKey && {t(card.descriptionKey)}} + + +
+ {formatValue()} +
+ {card.trend?.enabled && ( +
+ {card.trend.direction === 'up' ? ( + + ) : ( + + )} + {t(card.trend.textKey, card.trend.textParams)} +
+ )} +
+
+ ) +} + +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 (
-
{icon}
+
+ {IconComponent && } +
-

{title}

+

{t(activity.titleKey)}

{description}

- {time} + + {t(activity.timeKey, activity.timeParams)} +
) } + +interface QuickActionFromConfigProps { + action: DashboardAction +} + +function QuickActionFromConfig({ action }: QuickActionFromConfigProps) { + const { t } = useTranslation() + const IconComponent = iconMap[action.icon] + + return ( + + ) +} diff --git a/src/data/dashboard.json b/src/data/dashboard.json new file mode 100644 index 0000000..1ef0c9c --- /dev/null +++ b/src/data/dashboard.json @@ -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" + } + ] +} diff --git a/src/data/translations/en.json b/src/data/translations/en.json index 696346e..49af535 100644 --- a/src/data/translations/en.json +++ b/src/data/translations/en.json @@ -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", diff --git a/src/data/translations/es.json b/src/data/translations/es.json index 8795fa6..7e3f6b7 100644 --- a/src/data/translations/es.json +++ b/src/data/translations/es.json @@ -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", diff --git a/src/data/translations/fr.json b/src/data/translations/fr.json index c150357..090fe5e 100644 --- a/src/data/translations/fr.json +++ b/src/data/translations/fr.json @@ -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", diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d1a1726..1107850 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -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' diff --git a/src/hooks/use-dashboard-config.ts b/src/hooks/use-dashboard-config.ts new file mode 100644 index 0000000..5d1a670 --- /dev/null +++ b/src/hooks/use-dashboard-config.ts @@ -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 + } +} + +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 + color: string + } + actions?: DashboardAction[] +} + +export interface DashboardActivity { + id: string + icon: string + iconColor: string + titleKey: string + description?: string + descriptionKey?: string + descriptionParams?: Record + timeKey: string + timeParams?: Record + 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(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(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 + } +}