WIP jordan
This commit is contained in:
273
app/composables/useDashboardHomeWidgets.ts
Normal file
273
app/composables/useDashboardHomeWidgets.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user