# Dynamic Dashboard — Architecture & Flow Guide

This document describes how the **Dynamic Dashboard** (`/dashboard`) works after the shared-widget upgrade. Use it when modifying widgets, permissions, filters, or UI behavior.

---

## Overview

| Concept | Behavior |
|--------|----------|
| **Scope** | Widgets are **organization-wide** (`is_global = true`), not per-user |
| **Management** | Users with `dynamic dashboard` permissions (`add_widget`, `edit_widget`, `delete_widget`) create/edit/delete shared widgets |
| **Visibility** | All active users see the same widget set, minus **role** and **user** exclusions |
| **Data scope** | Each user still sees **data filtered by their own access** (department/role rules via `DynamicDashboardAccessService`) |
| **MEC users** | Redirected to classic dashboard (`/dashboard1`) — they do not use the dynamic dashboard |
| **Audience picker** | MEC role and MEC users are **never shown** in exclude lists (they cannot see widgets anyway) |

---

## Database: `dashboard_widgets`

| Column | Purpose |
|--------|---------|
| `id` | Primary key |
| `user_id` | Legacy creator reference (kept for compatibility) |
| `created_by` | User who created the widget |
| `is_global` | Must be `true` for shared dashboard widgets |
| `is_active` | Inactive widgets are hidden from everyone |
| `title` | Widget heading |
| `description` | Admin note (optional); can also appear as subtitle via display options |
| `data_source` | `bms` or `pms` |
| `config` | JSON — display, metrics, filters, display_options |
| `excluded_role_ids` | JSON array of role IDs — those roles **do not** see the widget |
| `excluded_user_ids` | JSON array of user IDs — those users **do not** see the widget |
| `width` | Bootstrap grid: 3, 4, 6, 8, 12 |
| `sort_order` | Global order (admin drag-reorder affects all users) |

### Visibility logic (`DashboardWidget::scopeVisibleTo`)

A widget is shown when **all** are true:

1. `is_global = true`
2. `is_active = true`
3. Viewer's `role_id` **not** in `excluded_role_ids`
4. Viewer's `id` **not** in `excluded_user_ids`

---

## Permissions

Configured in `config/permission.php` under module **Dashboard**:

| Permission slug | Actions | Purpose |
|----------------|---------|---------|
| `dashboard` | `list`, `view` | Access dashboard page & load widget data |
| `dynamic dashboard` | `add_widget` | Create shared widgets |
| `dynamic dashboard` | `edit_widget` | Edit, reorder, audience API |
| `dynamic dashboard` | `delete_widget` | Delete widgets |

Route mapping (middleware `permission.route`):

- `GET /dashboard` → `dashboard.list`
- `GET /dashboard/widgets/{id}/data` → `dashboard.list`
- `POST /dashboard/widgets` → `dynamic_dashboard.add_widget`
- `PUT /dashboard/widgets/{id}` → `dynamic_dashboard.edit_widget`
- `DELETE /dashboard/widgets/{id}` → `dynamic_dashboard.delete_widget`
- `POST /dashboard/widgets/reorder` → `dynamic_dashboard.edit_widget`
- `GET /dashboard/roles`, `GET /dashboard/users` → `dynamic_dashboard.edit_widget`

---

## API Routes

| Method | URI | Name | Description |
|--------|-----|------|-------------|
| GET | `/dashboard` | `dashboard` | Main page |
| GET | `/dashboard/schema` | `dashboard.schema` | Builder JSON schema |
| GET | `/dashboard/filter-options?source=bms\|pms` | `dashboard.filter-options` | Filter dropdown options |
| GET | `/dashboard/roles` | `dashboard.roles` | Roles for exclusion picker |
| GET | `/dashboard/users?role_ids[]=1` | `dashboard.users` | Users (optionally filtered by roles) |
| GET | `/dashboard/snapshots?year=` | `dashboard.snapshots` | Snapshot list for year filter |
| POST | `/dashboard/widgets` | `dashboard.widgets.store` | Create widget |
| PUT | `/dashboard/widgets/{widget}` | `dashboard.widgets.update` | Update widget |
| GET | `/dashboard/widgets/{widget}` | `dashboard.widgets.show` | Fetch widget for edit |
| DELETE | `/dashboard/widgets/{widget}` | `dashboard.widgets.destroy` | Delete widget |
| POST | `/dashboard/widgets/reorder` | `dashboard.widgets.reorder` | Save global order |
| GET | `/dashboard/widgets/{widget}/data` | `dashboard.widgets.data` | Widget dataset (JSON) |

---

## Widget Builder (5 Steps)

Frontend: `public/js/dynamic-dashboard.js` + `resources/views/dynamic-dashboard/partials/widget-modal.blade.php`

| Step | Name | Content |
|------|------|---------|
| 1 | Source | BMS or PMS |
| 2 | Filters | Multi-select filters, group-by, PMS entity |
| 3 | Metrics | BMS financial metrics or PMS count |
| 4 | Display | Title, description, chart type, sort, width, **advanced controls** |
| 5 | Audience | Active toggle, exclude roles, exclude users |

### Audience rules

- **Default:** visible to all users
- **Exclude roles:** multi-select — users with those roles never see the card
- **Exclude users:** multi-select — if roles are selected first, user list is **filtered by those roles**; if no roles selected, **all active users** are listed

---

## Widget `config` JSON Shape

```json
{
  "display_type": "card|table|pie|bar|line|map",
  "sort_arrangement": "value_desc|value_asc|label_asc|label_desc",
  "metrics": ["count", "cost"],
  "filters": {
    "year": ["2026-2027"],
    "group_by": "department",
    "project_type": ["On-Going"],
    "is_live_mode": false
  },
  "entity": "pc_forms",
  "display_options": {
    "subtitle": "Optional subtitle",
    "help_text": "Shown under widget content",
    "empty_message": "No data matches the current filters.",
    "chart_limit": 18,
    "show_legend": true,
    "compact_numbers": false,
    "hide_zero_rows": false
  }
}
```

- `entity` — PMS only
- `group_by` — stored inside `filters`
- `snapshot_id: "live"` → converted to `is_live_mode: true` on save

---

## BMS Filters (BPMS IndexV2 parity)

Filters are grouped in the widget builder (Budget & View, Organization, Scheme Category, Status, Identifiers, Financial Ranges, Dates).

**Toolbar / scope (same as BPMS dashboard):** `year`, `snapshot_id` (Live / At Publish), `year_from`, `year_to`, `department_id`, `sub_department_id`, `sector_id`, `sub_sector_id`, `blk_id`, `fund_source_id`, `location_id`

**Scheme:** `category`, `project_type`, `adp_type`, `completion_status`, `status`, `published`, `approved`, `under_review`, `work_type_id`, `mec_status`, `is_disabled`

**Drill-down / modal (CategoryProjectsModal parity):** `search`, `code`, `adp_number`, `project_name`, `department_name`, `project_id_from`, `project_id_to`, numeric ranges (`cost_*`, `expenditure_upto_previous_*`, `throwforward_*`, `current_allocation_*`, `fpa_*`, `current_expenditure_*`, `total_release_upto_current_*`, `total_expenditure_upto_current_*`), date ranges (`created_at_*`, `updated_at_*`, `target_completion_date_*`)

**Categories:** `total`, `ongoing`, `new`, `non_adp`, `major_flagship`, `ltbc`, `ltbc_le50`, `ltbc_gt50`, `fpa`, `ddwp`, `pdwp`, `token_allocation`, `zero_release`, `under_revision`, `sdg_no_blk`, `block_schemes`, `sector_wise`, `scheme_that_year`, `all`

**Not in widgets:** BPMS Release/Expenditure import batch overlay (activity-log insight only; no PMS equivalent).

**Note:** Financial range, text search, and date filters apply fully to grouped charts and project-backed widgets. Single category cards re-aggregate from filtered projects when those advanced filters are set.

## PMS Filters

`year`, `snapshot_id`, `department_id`, `pc_form_kind`, `pc_form_state`, `plan_type`, `plan_status`, `report_type` (entity-scoped)

---

## Backend Service Layer

| Service | Responsibility |
|---------|----------------|
| `DynamicDashboardWidgetService` | CRUD, global reorder, visibility listing |
| `DynamicDashboardAudienceService` | Role/user options for builder |
| `DynamicDashboardConfigService` | Schema, filter metadata, option labels |
| `DynamicDashboardAccessService` | Per-user data scoping (departments) |
| `DynamicDashboardDataService` | Fetch & format widget data (10-min cache per user+config) |
| `DynamicDashboardBmsService` | BMS data via `ProjectService::getFinancialAnalysis()` |
| `DynamicDashboardController` | HTTP layer |

### Data flow (widget load)

```
Browser → GET /dashboard/widgets/{id}/data
       → DynamicDashboardController::widgetData
       → assertWidgetVisible (role/user exclusions)
       → DynamicDashboardDataService::fetchWidgetData
       → BMS: DynamicDashboardBmsService / PMS: internal queries
       → JSON → Chart.js / MapLibre / card HTML
```

### Data flow (save widget)

```
Admin → 5-step modal → POST/PUT /dashboard/widgets
     → validateWidgetPayload
     → DynamicDashboardWidgetService::create/update
     → All eligible users see widget on next page load
```

---

## Frontend Files

| File | Role |
|------|------|
| `resources/views/dynamic-dashboard/index.blade.php` | Page shell, config bootstrap |
| `resources/views/dynamic-dashboard/partials/widget-card.blade.php` | Widget shell (card/panel) |
| `resources/views/dynamic-dashboard/partials/widget-modal.blade.php` | 5-step builder |
| `public/js/dynamic-dashboard.js` | Wizard, CRUD, charts, maps, audience |
| `public/css/dynamic-dashboard.css` | Themed layout |

Bootstrap config: `window.ddDashboardConfig` (routes, schema, permissions, roleOptions)

---

## UI States

| State | Where | Behavior |
|-------|-------|----------|
| **Loading** | Widget card overlay | Spinner while fetching `/data` |
| **Error** | Widget card overlay | Message + Retry button |
| **Empty dashboard** | `#dd-empty-state` | No visible widgets for user |
| **Empty data** | Widget content | Controlled by `display_options.empty_message` |
| **Filter loading** | Step 2 modal | Spinner + retry on failure |
| **User loading** | Step 5 modal | Spinner while fetching users |
| **Save** | Modal footer | Disabled button + spinner |

---

## Seeder

`database/seeders/DynamicDashboardWidgetsSeeder.php` — seeds **global** default widgets (replaces existing global widgets). Run:

```bash
php artisan db:seed --class=DynamicDashboardWidgetsSeeder
```

---

## Customization Tips

1. **New BMS filter** — Add to `DynamicDashboardConfigService::bmsFilters()`, options in `bmsFilterOptions()`, wire in `ProjectService` query if needed, update `DynamicDashboardBmsService::requestQueryFromFilters()`.
2. **New metric** — Add to `bmsMetrics()` and ensure `DynamicDashboardBmsService` bucket keys include it.
3. **New display type** — Schema + JS `renderWidgetContent` + CSS + validation rule in controller.
4. **Stricter audience** — Extend `DashboardWidget::scopeVisibleTo` (e.g. department allow-list).
5. **Per-user layout** — Would require new table (not implemented); current design is single global layout.

---

## Migration

```bash
php artisan migrate
```

Migration: `2026_07_01_100000_add_global_audience_to_dashboard_widgets.php`

---

*Last updated: shared dashboard with role/user exclusions and 5-step builder.*
