Dashboard Widgets

Modules can provide custom dashboard widgets that display real-time data. This guide covers creating widget data providers and registering them with the system.

Overview

The widget system consists of:

  1. Widget Definitions - Metadata about the widget (name, size, category, config schema)
  2. Widget Data Providers - Backend classes that fetch and format data
  3. Widget Registry - Central registry that manages widget registration

Creating a Widget Data Provider

1. Create the Provider Class

Create a class that extends BaseWidgetDataProvider:

<?php

namespace Modules\YourModule\App\WidgetProviders;

use App\Core\Services\BaseWidgetDataProvider;
use Modules\YourModule\App\Models\YourModel;

class YourWidgetProvider extends BaseWidgetDataProvider
{
    /**
     * Cache TTL in seconds (default: 300 = 5 minutes)
     */
    protected int $cacheTtl = 300;

    /**
     * Optional: Config schema for validation
     */
    protected array $configSchema = [
        'type' => 'object',
        'properties' => [
            'showTrend' => ['type' => 'boolean'],
            'limit' => [
                'type' => 'integer',
                'minimum' => 1,
                'maximum' => 100,
            ],
        ],
    ];

    /**
     * Unique widget identifier
     */
    public static function getWidgetId(): string
    {
        return 'yourmodule:your-widget';
    }

    /**
     * Fetch and return widget data
     */
    public function getData(array $config = [], array $context = []): array
    {
        $dateRange = $this->getDateRange($context);
        $limit = $config['limit'] ?? 10;

        // Query your data
        $currentStats = $this->getStats($dateRange['start'], $dateRange['end']);

        // Get previous period for comparison
        $periodDays = $dateRange['start']->diffInDays($dateRange['end']);
        $previousStats = $this->getStats(
            $dateRange['start']->copy()->subDays($periodDays),
            $dateRange['start']->copy()->subDay()
        );

        return [
            'total' => [
                'value' => $currentStats['total'],
                'formatted' => $this->formatNumber($currentStats['total']),
                'change' => $this->calculateChange(
                    $currentStats['total'],
                    $previousStats['total']
                ),
            ],
            'items' => $this->getRecentItems($limit),
            'period' => [
                'start' => $dateRange['start']->toIso8601String(),
                'end' => $dateRange['end']->toIso8601String(),
            ],
        ];
    }

    protected function getStats($start, $end): array
    {
        return YourModel::query()
            ->whereBetween('created_at', [$start, $end])
            ->selectRaw('COUNT(*) as total, SUM(amount) as revenue')
            ->first()
            ->toArray();
    }

    protected function getRecentItems(int $limit): array
    {
        return YourModel::query()
            ->latest()
            ->limit($limit)
            ->get(['id', 'name', 'amount', 'created_at'])
            ->toArray();
    }
}

2. Register in module.json

Add the provider to your module's configuration:

{
  "backend": {
    "widgetDataProviders": {
      "yourmodule:your-widget": "Modules\\YourModule\\App\\WidgetProviders\\YourWidgetProvider"
    }
  }
}

The system will automatically register the provider when your module loads.

Base Provider Features

BaseWidgetDataProvider provides several helpful features:

Caching

Data is automatically cached based on the $cacheTtl property:

protected int $cacheTtl = 300; // 5 minutes

// Or disable caching
protected int $cacheTtl = 0;

Cache keys are generated based on widget ID, config, and context.

Date Range Helpers

$dateRange = $this->getDateRange($context);
// Returns: ['start' => Carbon, 'end' => Carbon]

Number Formatting

$this->formatNumber(1234567);    // "1.2M"
$this->formatNumber(12345);      // "12.3K"
$this->formatNumber(123);        // "123"

Change Calculation

$change = $this->calculateChange($current, $previous);
// Returns: ['value' => 12.5, 'direction' => 'up']

Tenant Context

$tenantId = $this->getTenantId($context);

Config Validation

Define a JSON Schema-like $configSchema to validate widget configuration:

protected array $configSchema = [
    'type' => 'object',
    'properties' => [
        'refreshInterval' => [
            'type' => 'integer',
            'minimum' => 30,
            'maximum' => 3600,
        ],
        'displayMode' => [
            'type' => 'string',
            'enum' => ['compact', 'detailed', 'chart'],
        ],
        'showLegend' => [
            'type' => 'boolean',
        ],
    ],
    'required' => ['displayMode'],
];

Supported validation rules:

Rule Description
type string, number, integer, boolean, array, object
required Array of required property names
enum Array of allowed values
minimum / maximum Number range
minLength / maxLength String length
pattern Regex pattern
format email, url, date, uuid, color
minItems / maxItems Array length
properties Nested object validation

Widget API Endpoints

The dashboard module provides these API endpoints:

Get Widget Data

GET /api/v1/widgets/data/{widgetId}

Query params:

  • config - Widget configuration (JSON)
  • dateRange - Date range context

Batch Data Fetch

POST /api/v1/widgets/data/batch

Body:

{
  "widgets": [
    { "widgetId": "orders:overview", "config": {} },
    { "widgetId": "customers:stats", "config": { "limit": 5 } }
  ],
  "dateRange": {
    "start": "2024-01-01",
    "end": "2024-01-31"
  }
}

Refresh Widget Data

POST /api/v1/widgets/data/{widgetId}/refresh

Forces a cache refresh and returns fresh data.

Validate Config

POST /api/v1/widgets/config/{widgetId}/validate

Body:

{
  "config": {
    "displayMode": "chart",
    "showLegend": true
  }
}

Get Config Schema

GET /api/v1/widgets/config/{widgetId}/schema

Returns the widget's configuration schema.

Custom Widget Categories

Register custom categories for organizing widgets:

{
  "backend": {
    "widgetCategories": {
      "yourmodule": {
        "label": "Your Module",
        "icon": "Box",
        "description": "Widgets for Your Module analytics"
      }
    }
  }
}

Default categories include:

  • analytics - Data visualization and metrics
  • orders - Order tracking and management
  • customers - Customer insights
  • inventory - Stock and product management
  • finance - Revenue and financial data
  • workflows - Automation metrics

Best Practices

  1. Use Caching - Always set an appropriate $cacheTtl for production
  2. Handle Empty Data - Return sensible defaults when no data exists
  3. Validate Config - Define $configSchema for complex configurations
  4. Use Context - Respect date ranges and tenant context from $context
  5. Format Numbers - Use formatNumber() for large values
  6. Include Trends - Use calculateChange() for period-over-period comparisons

Example: Orders Overview Widget

<?php

namespace Modules\Orders\App\WidgetProviders;

use App\Core\Services\BaseWidgetDataProvider;
use Modules\Orders\App\Models\Order;

class OrdersOverviewProvider extends BaseWidgetDataProvider
{
    protected int $cacheTtl = 300;

    public static function getWidgetId(): string
    {
        return 'orders:overview';
    }

    public function getData(array $config = [], array $context = []): array
    {
        $dateRange = $this->getDateRange($context);

        $stats = Order::query()
            ->whereBetween('created_at', [$dateRange['start'], $dateRange['end']])
            ->selectRaw('
                COUNT(*) as total_orders,
                COALESCE(SUM(total_amount), 0) as total_revenue,
                COALESCE(AVG(total_amount), 0) as avg_order_value,
                SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as pending,
                SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as shipped
            ', ['pending', 'shipped'])
            ->first();

        return [
            'totalOrders' => [
                'value' => $stats->total_orders,
                'formatted' => $this->formatNumber($stats->total_orders),
            ],
            'totalRevenue' => [
                'value' => $stats->total_revenue,
                'formatted' => '₹' . $this->formatNumber($stats->total_revenue),
            ],
            'avgOrderValue' => [
                'value' => $stats->avg_order_value,
                'formatted' => '₹' . number_format($stats->avg_order_value, 2),
            ],
            'pendingOrders' => $stats->pending,
            'shippedOrders' => $stats->shipped,
        ];
    }
}

Next Steps