Files
policy-ui/app/composables/useDashboardHomeWidgets.ts
Jordan Weingarten 67482f6629 WIP jordan
2026-04-16 11:11:44 -05:00

274 lines
7.7 KiB
TypeScript

/**
* Home dashboard widget visibility — role presets + per-widget toggles.
* Persisted locally until per-user API exists.
*/
export type DashboardWidgetId =
| 'hero'
| 'milestone'
| 'performance'
| 'tasks_alerts'
| 'charts'
| 'brokerage_health'
| 'quotes_line'
| 'notes'
| 'calendar'
| 'quick_leads'
| 'sales_leads'
| 'client_favorites'
| 'drafts'
export type DashboardRolePresetId =
| 'sales_manager'
| 'executive_manager'
| 'director'
| 'financial'
| 'admin_manager'
| 'customer_service_manager'
export type DashboardWidgetMeta = {
id: DashboardWidgetId
label: string
description: string
}
export const DASHBOARD_WIDGETS: DashboardWidgetMeta[] = [
{ id: 'hero', label: 'Welcome banner', description: 'Greeting, CTAs, workspace strip' },
{ id: 'milestone', label: 'MTD milestone', description: 'Plan vs actual snapshot' },
{ id: 'tasks_alerts', label: 'Tasks & alerts', description: 'Daily work + exceptions' },
{ id: 'performance', label: 'Today at a glance', description: 'Headline KPIs + sparklines' },
{ id: 'charts', label: 'Charts', description: 'GWP trend & quoted pipeline' },
{ id: 'brokerage_health', label: 'Brokerage health', description: 'YTD / trailing book metrics' },
{ id: 'quotes_line', label: 'Sent quotes', description: 'Sortable list of quotes sent to clients' },
{ id: 'notes', label: 'Notes', description: 'Personal scratchpad and reminders' },
{ id: 'calendar', label: 'Calendar', description: 'Agenda, renewals, alerts & reminders' },
{ id: 'quick_leads', label: 'Quick leads', description: 'Recent quick leads from the last 10 days' },
{ id: 'sales_leads', label: 'Sales leads', description: 'All leads by source — filter by channel, campaign, or API' },
{ id: 'client_favorites', label: 'Favorite clients', description: 'Starred clients for quick access' },
{ id: 'drafts', label: 'Drafts', description: 'Resume in-progress quotes, solicitudes & registrations' }
]
const STORAGE_KEY = 'policy-ui.dashboard.widgets.v4'
export const DEFAULT_WIDGET_ORDER: DashboardWidgetId[] = DASHBOARD_WIDGETS.map((w) => w.id)
function normalizeWidgetOrder(raw: unknown): DashboardWidgetId[] {
const base = [...DEFAULT_WIDGET_ORDER]
if (!Array.isArray(raw)) return base
const seen = new Set<DashboardWidgetId>()
const out: DashboardWidgetId[] = []
for (const x of raw) {
if (typeof x === 'string' && base.includes(x as DashboardWidgetId) && !seen.has(x as DashboardWidgetId)) {
const id = x as DashboardWidgetId
seen.add(id)
out.push(id)
}
}
for (const id of base) {
if (!seen.has(id)) out.push(id)
}
return out
}
const ALL_ON: Record<DashboardWidgetId, boolean> = {
hero: true,
milestone: true,
performance: false,
tasks_alerts: true,
charts: true,
brokerage_health: true,
quotes_line: true,
notes: true,
calendar: true,
quick_leads: true,
sales_leads: true,
client_favorites: true,
drafts: true
}
export const DASHBOARD_ROLE_PRESETS: Record<
DashboardRolePresetId,
{ label: string; hint: string; widgets: Record<DashboardWidgetId, boolean> }
> = {
sales_manager: {
label: 'Sales manager',
hint: 'Pipeline, tasks, quotes — lighter book-of-business tile.',
widgets: { ...ALL_ON, brokerage_health: false }
},
executive_manager: {
label: 'Executive manager',
hint: 'Balanced operational + book view.',
widgets: { ...ALL_ON }
},
director: {
label: 'Director',
hint: 'Strategic KPIs & health; fewer operational tiles.',
widgets: {
...ALL_ON,
tasks_alerts: false,
quotes_line: false
}
},
financial: {
label: 'Financial',
hint: 'Premium, AR, health metrics; fewer sales shortcuts.',
widgets: {
...ALL_ON,
quotes_line: false,
tasks_alerts: true,
performance: false,
brokerage_health: true,
charts: true,
sales_leads: false
}
},
admin_manager: {
label: 'Admin / operations',
hint: 'Permissions, forms, and carrier setup — fewer quote shortcuts.',
widgets: {
...ALL_ON,
quotes_line: false,
charts: false,
brokerage_health: true,
sales_leads: false
}
},
customer_service_manager: {
label: 'Customer service manager',
hint: 'Queues, tasks, and exceptions — lighter GWP / book tiles.',
widgets: {
...ALL_ON,
charts: false,
brokerage_health: false,
quotes_line: false
}
}
}
/** Stable order for selects. */
export const DASHBOARD_PRESET_ORDER: DashboardRolePresetId[] = [
'sales_manager',
'executive_manager',
'director',
'financial',
'admin_manager',
'customer_service_manager'
]
function cloneWidgets(w: Record<DashboardWidgetId, boolean>): Record<DashboardWidgetId, boolean> {
return { ...w }
}
export function useDashboardHomeWidgets() {
const activePreset = ref<DashboardRolePresetId>('executive_manager')
const widgets = ref<Record<DashboardWidgetId, boolean>>(
cloneWidgets(DASHBOARD_ROLE_PRESETS.executive_manager.widgets)
)
const widgetOrder = ref<DashboardWidgetId[]>([...DEFAULT_WIDGET_ORDER])
const layoutUnlocked = ref(false)
const hydrated = ref(false)
function persist() {
if (typeof localStorage === 'undefined') return
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
preset: activePreset.value,
widgets: widgets.value,
widgetOrder: widgetOrder.value,
layoutUnlocked: layoutUnlocked.value
})
)
} catch {
/* quota */
}
}
function load() {
if (typeof localStorage === 'undefined') return
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return
const data = JSON.parse(raw) as {
preset?: DashboardRolePresetId
widgets?: Partial<Record<DashboardWidgetId, boolean>>
widgetOrder?: DashboardWidgetId[]
layoutUnlocked?: boolean
}
if (data.preset && DASHBOARD_ROLE_PRESETS[data.preset]) {
activePreset.value = data.preset
}
if (data.widgets) {
const merged = { ...widgets.value }
for (const k of Object.keys(merged) as DashboardWidgetId[]) {
if (data.widgets[k] !== undefined) merged[k] = data.widgets[k]!
}
widgets.value = merged
}
widgetOrder.value = normalizeWidgetOrder(data.widgetOrder)
if (typeof data.layoutUnlocked === 'boolean') {
layoutUnlocked.value = data.layoutUnlocked
}
} catch {
/* ignore */
}
}
onMounted(() => {
load()
hydrated.value = true
})
watch(
[activePreset, widgets, widgetOrder, layoutUnlocked],
() => {
if (hydrated.value) persist()
},
{ deep: true }
)
const isPresetDirty = computed(() => {
const preset = DASHBOARD_ROLE_PRESETS[activePreset.value]
if (!preset) return false
return DASHBOARD_WIDGETS.some((w) => widgets.value[w.id] !== preset.widgets[w.id])
})
function applyPreset(id: DashboardRolePresetId) {
activePreset.value = id
const p = DASHBOARD_ROLE_PRESETS[id]
if (p) widgets.value = cloneWidgets(p.widgets)
}
function setWidget(id: DashboardWidgetId, on: boolean) {
widgets.value = { ...widgets.value, [id]: on }
}
function reapplySelectedPreset() {
applyPreset(activePreset.value)
}
function reorderWidgets(fromId: DashboardWidgetId, toId: DashboardWidgetId) {
if (fromId === toId) return
const arr = [...widgetOrder.value]
const fromI = arr.indexOf(fromId)
const toI = arr.indexOf(toId)
if (fromI === -1 || toI === -1) return
arr.splice(fromI, 1)
arr.splice(toI, 0, fromId)
widgetOrder.value = arr
}
return {
widgets,
widgetOrder,
layoutUnlocked,
activePreset,
isPresetDirty,
applyPreset,
setWidget,
reapplySelectedPreset,
reorderWidgets
}
}