feat: shared widget filter system for all dashboard widgets
Shared infrastructure: - WidgetFilterBar: declarative filter component (search, select, toggle) - useWidgetFilterOptions: cached hook for clients, countries, roles, chapters Widget integration (5 widgets): - ProjectHealth: search (name) + select (client) - BudgetForecast: search (name) + select (client) - Chargeability: select (chapter) + toggle (include proposed) - SkillGap: search (skill name) - TopValue: select (chapter) Backend: added clientId/clientName to ProjectHealth and BudgetForecast query results for client-based filtering. Filter state persisted via widget config (survives page reload). All filters use compact 11px inputs with full dark theme support. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Shared hook for loading filter options used across dashboard widgets.
|
||||
* Loads clients, countries, roles, and chapters once with long cache TTL.
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
|
||||
export interface FilterOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function useWidgetFilterOptions() {
|
||||
const { data: clientsRaw } = trpc.clientEntity.list.useQuery(
|
||||
{ isActive: true },
|
||||
{ staleTime: 300_000 },
|
||||
);
|
||||
|
||||
const { data: countriesRaw } = trpc.country.list.useQuery(
|
||||
{ isActive: true },
|
||||
{ staleTime: 300_000 },
|
||||
);
|
||||
|
||||
const { data: rolesRaw } = trpc.role.list.useQuery(
|
||||
{ isActive: true },
|
||||
{ staleTime: 300_000 },
|
||||
);
|
||||
|
||||
const clients = useMemo<FilterOption[]>(() => {
|
||||
const list = (Array.isArray(clientsRaw) ? clientsRaw : (clientsRaw as any)?.clients ?? []) as Array<{ id: string; name: string }>;
|
||||
return list.map((c) => ({ value: c.id, label: c.name }));
|
||||
}, [clientsRaw]);
|
||||
|
||||
const countries = useMemo<FilterOption[]>(() => {
|
||||
const list = (Array.isArray(countriesRaw) ? countriesRaw : []) as Array<{ id: string; name: string }>;
|
||||
return list.map((c) => ({ value: c.id, label: c.name }));
|
||||
}, [countriesRaw]);
|
||||
|
||||
const roles = useMemo<FilterOption[]>(() => {
|
||||
const list = (Array.isArray(rolesRaw) ? rolesRaw : []) as Array<{ id: string; name: string }>;
|
||||
return list.map((r) => ({ value: r.id, label: r.name }));
|
||||
}, [rolesRaw]);
|
||||
|
||||
// Chapters are derived from roles or can be hardcoded common ones
|
||||
const chapters = useMemo<FilterOption[]>(() => {
|
||||
const common = [
|
||||
"Digital Content Production",
|
||||
"Project Management",
|
||||
"Art Direction",
|
||||
"CGI-Dev",
|
||||
"Product Data Management",
|
||||
];
|
||||
return common.map((c) => ({ value: c, label: c }));
|
||||
}, []);
|
||||
|
||||
return { clients, countries, roles, chapters };
|
||||
}
|
||||
Reference in New Issue
Block a user