Skip to main content

Documentation Index

Fetch the complete documentation index at: https://grantmaster.dev/llms.txt

Use this file to discover all available pages before exploring further.

Widget System

Overview

The widget system is the foundation of GrantMaster’s customisable dashboard experience. Each widget is a self-contained React component that displays a specific piece of information or functionality. Widgets are registered in Firestore, assigned to roles by a Superadmin, and rendered in a 12-column responsive grid. Users and organisations can further customise which widgets they see and in what order. The system has four layers:
  1. Registry — Firestore-backed catalogue of widget definitions and role assignments
  2. Templates — default dashboard layouts per role, applied on user onboarding
  3. Overrides — per-organisation visibility and ordering changes
  4. Preferences — per-user hidden-widget list and custom ordering

Widget Types and Sizes

Size Enum (WidgetSize)

The grid uses 12 columns with 8 pt spacing. Each WidgetSize value maps to a column span and a pixel height:
Enum valueColumnsHeightTypical use
COMPACT4160 pxSmall counters, quick-action buttons
MEDIUM6160 pxSummary cards with a small chart
WIDE8160 pxHorizontal lists or progress bars
TALL4320 pxNarrow vertical lists
CHART8320 pxLine/bar charts
LARGE12320 pxFull-width tables or complex views
Deprecated aliases (kept for backward compatibility, removed in v2.0):
AliasResolves to
SMALLCOMPACT
FULLLARGE

Category Enum (WidgetCategory)

Categories group widgets in the Add Widget modal and the assignment matrix:
ValueDescription
PERSONALUser-specific metrics (leave balance, journals)
PROJECTSProject status, milestones, budget burn
FINANCEExpense summaries, financial KPIs
GRANTSGrant pipeline, submission deadlines
USERSTeam headcount, capacity
MISSIONImpact indicators, M&E summaries
COMPLIANCECompliance scores, overdue tasks
PLATFORMSystem health, subscription usage

Height Enum (WidgetHeight)

WidgetHeight is a string-union used when a widget needs to declare an explicit pixel height outside the standard WidgetSize row heights. It is less commonly used than WidgetSize.

Loading State Enum (WidgetLoadingState)

enum WidgetLoadingState {
  IDLE    = 'IDLE',
  LOADING = 'LOADING',
  SUCCESS = 'SUCCESS',
  ERROR   = 'ERROR',
}
Used by useWidgets to expose fine-grained loading state beyond a simple boolean.

Core Type Definitions

All widget types are defined in src/shared/platform/widget.contracts.ts.

WidgetDefinition

The canonical record stored in Firestore for each widget:
interface WidgetDefinition {
  id: string;                   // unique slug, e.g. 'grant-pipeline-summary'
  name: string;
  description: string;
  category: WidgetCategory;
  defaultSize: WidgetSize;
  supportedSizes: WidgetSize[]; // sizes the component can render at
  componentPath: string;        // path for dynamic import, e.g. 'widgets/grants/PipelineSummary'
  icon?: string;                // lucide-react icon name
  isActive: boolean;
  noFrame?: boolean;            // widget provides its own container; skip default Card wrapper
  configSchema?: WidgetConfigSchema; // Zod schema for per-instance settings
  metadata?: WidgetMetadata;
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

WidgetAssignment

Links a widget to zero or more roles:
interface WidgetAssignment {
  id: string;
  widgetId: string;
  assignedRoles: SystemRole[];
  priority: number;   // default sort order within the role's dashboard
  isActive: boolean;
  updatedAt: Timestamp;
  updatedBy: string;
}

WidgetOverride

Per-organisation customisation stored under organizations/{orgId}/widgetOverrides/{widgetId}:
interface WidgetOverride {
  widgetId: string;
  organizationId: string;
  isHidden?: boolean;
  displayName?: string;     // rename for this org
  priority?: number;        // reorder within this org
  updatedAt: Timestamp;
  updatedBy: string;
}

WidgetInstance

What is actually rendered — definition merged with any org override:
interface WidgetInstance {
  definition: WidgetDefinition;
  override?: WidgetOverride;
  currentSize: WidgetSize;
}

WidgetPreferences

Per-user preferences stored in the user’s Firestore document (employees/{uid}.widgetPreferences):
interface WidgetPreferences {
  hiddenWidgets: string[];               // widgetIds to hide
  widgetOrder: Array<{
    widgetId: string;
    priority: number;
  }>;
  widgetConfig?: Record<string, WidgetConfig>; // per-widget user settings
}

WidgetProps

Standard props injected into every widget component:
interface WidgetProps {
  widgetId: string;
  size: WidgetSize;
  config?: WidgetConfig;          // resolved user/org config
  onConfigChange?: (config: WidgetConfig) => void;
}

Widget Registration

Firestore Storage Paths

DataFirestore path
Widget definitionsplatform/widgets/definitions/{widgetId}
Role assignmentsplatform/widgets/assignments/{assignmentId}
Org overridesorganizations/{orgId}/widgetOverrides/{widgetId}

WidgetRegistryService

src/shared/platform/widgetRegistry.ts is the single access point for all Firestore widget operations. It is a static service class with a 5-minute in-memory cache that is shared across the session. Key methods:
MethodDescription
getAllWidgets()All active WidgetDefinition records
getWidget(id)Single definition by id
saveWidget(def)Create or update a definition
deleteWidget(id)Soft-delete (sets isActive: false)
getAllAssignments()All WidgetAssignment records
getWidgetsByRole(role)Definitions assigned to a specific role
getWidgetsForUser(role, orgId)Full resolution pipeline (role → org overrides → instances)
bulkUpdateAssignments(changes, updatedBy, priorities?)Batch-write assignment changes
filterWidgets(filter)Filter definitions by category, size, or active flag
clearCache()Invalidate the in-memory cache
Superadmin shortcut: getWidgetsForUser returns ALL active definitions when the role is SystemRole.SUPER_ADMIN, skipping the assignment lookup.

Adding a New Built-in Widget

  1. Create the component under src/widgets/[category]/MyWidget.tsx. It must accept WidgetProps.
  2. Register it in Firestore (via the Superadmin Widget Library UI or a seed script) with componentPath matching the file path.
  3. Create a WidgetAssignment record linking it to the appropriate roles.
  4. (Optional) Add a WidgetConfigSchema if the widget has user-configurable settings.
  5. Add a WidgetTemplate entry to the relevant ROLE_TEMPLATES in src/config/dashboardTemplates.ts.

Extension / Marketplace Widgets

Marketplace modules (the Extensions feature) can contribute widgets by registering additional WidgetDefinition records in Firestore with the same schema. No code change is required in the host app — the registry service picks them up automatically via getAllWidgets(). The module’s component bundle must be deployed separately and referenced via componentPath.

Role-Based Dashboard Templates

Purpose

Templates define the default widget layout applied to a new user of a given role during onboarding. They are also used when a Superadmin clicks “Apply to existing users”.

RoleTemplate and WidgetTemplate

Defined in src/config/dashboardTemplates.ts:
interface WidgetTemplate {
  widgetId: string;
  priority: number;       // lower = higher on dashboard
  size: WidgetSize;
  category: WidgetCategory;
  rationale?: string;     // dev-facing explanation of why this widget is included
}

interface RoleTemplate {
  role: SystemRole;
  name: string;
  description: string;
  widgets: WidgetTemplate[];
}

Pre-built Templates

ROLE_TEMPLATES contains one entry for each of the five roles:
RoleFocus
SUPER_ADMINPlatform health, subscription metrics, cross-org overview
ADMINOrganisation KPIs, compliance, team capacity
MANAGERProject status, budget burn, journals requiring approval
MEMBERPersonal tasks, leave balance, assigned project widgets
AUDITORCompliance scores, audit trail, financial summaries

Helper Functions

// Retrieve the default template for a role
getDefaultDashboardTemplate(role: SystemRole): RoleTemplate | undefined

// Convert a RoleTemplate into WidgetPreferences (for writing to user profile)
templateToWidgetPreferences(template: RoleTemplate): WidgetPreferences

Widget Rendering and Layout

useWidgets Hook

src/hooks/useWidgets.ts — primary hook for loading the current user’s widget instances:
const { widgets, loading, error, refresh, loadingState } = useWidgets(includeHidden?);
Resolution order:
  1. Fetch WidgetAssignment records for the user’s role via WidgetRegistryService.getWidgetsForUser.
  2. Apply org-level overrides (hidden, renamed, reordered).
  3. Filter out widgets in currentUserProfile.widgetPreferences.hiddenWidgets (unless includeHidden is true).
  4. Sort by widgetPreferences.widgetOrder priorities if defined.
loading is a simple boolean derived from loadingState === WidgetLoadingState.LOADING. Use loadingState when you need to differentiate IDLE, SUCCESS, or ERROR states.

Other Hooks

HookPurpose
useAllWidgets(filter?)Superadmin only — all definitions, optional WidgetFilter
useWidgetAssignments()Manages the role ↔ widget assignment matrix; exposes toggleAssignment, saveChanges, reset
useWidgetStats()Aggregate counts: total active widgets and per-category breakdown

DashboardTemplateEditor (Superadmin UI)

src/features/dashboard/components/DashboardTemplateEditor/ implements the Superadmin template editing screen as a compound component:
<DashboardTemplateEditor>
  <DashboardTemplateEditor.Header />
  <DashboardTemplateEditor.InfoCard />
  <DashboardTemplateEditor.RoleTabs />
  <DashboardTemplateEditor.TemplateCanvas />
  <DashboardTemplateEditor.Actions />
</DashboardTemplateEditor>
State is managed via DashboardTemplateEditorContext which tracks:
  • templates: Map<SystemRole, RoleTemplate> — one entry per role
  • activeRole — the role tab currently selected
  • isDirty — whether unsaved changes exist
  • allWidgets — full widget definition catalogue from useAllWidgets
  • saving / loading — async operation states
handleSave(applyToExisting: boolean) writes templates to Firestore and, if applyToExisting is true, calls applyTemplateToRole to update all existing users of that role.

noFrame Widgets

When WidgetDefinition.noFrame is true, the dashboard renderer skips the default Card wrapper and renders the component directly into the grid cell. Use this for widgets that need a custom background, full-bleed images, or a map canvas.

Grid Rendering

The dashboard grid reads the currentSize from each WidgetInstance and applies Tailwind col-span-* classes accordingly:
WidgetSizeTailwind class
COMPACTcol-span-4
MEDIUMcol-span-6
WIDEcol-span-8
TALLcol-span-4
CHARTcol-span-8
LARGEcol-span-12
Row heights are applied via inline style using the pixel values from the WidgetHeight enum.

Widget Configuration

Widgets that have user-configurable settings declare a WidgetConfigSchema:
interface WidgetConfigSchema {
  fields: Array<{
    key: string;
    label: string;
    type: 'text' | 'number' | 'boolean' | 'select';
    defaultValue?: unknown;
    options?: Array<{ label: string; value: string }>; // for 'select'
  }>;
}
Resolved config is passed into the widget as the config prop. The widget calls onConfigChange to persist changes back to widgetPreferences.widgetConfig[widgetId] on the user’s Firestore document.

Cache Management

WidgetRegistryService maintains a module-level in-memory cache with a 5-minute TTL. The cache covers getAllWidgets() and getAllAssignments() responses.
  • Call WidgetRegistryService.clearCache() or refresh() from useWidgets / useAllWidgets after any mutation that should propagate immediately.
  • The cache is process-scoped (not shared across browser tabs or server instances).