import { z } from 'zod' import { JOBS_TIME_FILTER_VALUES } from '@/lib/jobs-time-filter' import { IDEA_STATUS_API_VALUES, type IdeaStatusApi } from '@/lib/idea-status' const PriorityFilter = z.union([ z.number().int().min(1).max(4), z.literal('all'), ]) const SortDir = z.enum(['asc', 'desc']) const SprintBacklogPrefs = z.object({ filterPriority: PriorityFilter.optional(), filterStatus: z.enum(['OPEN', 'IN_SPRINT', 'DONE', 'all']).optional(), sort: z.enum(['priority', 'status', 'code']).optional(), sortDir: SortDir.optional(), collapsedPbis: z.array(z.string()).optional(), filterPopoverOpen: z.boolean().optional(), }).strict() const PbiListPrefs = z.object({ sort: z.enum(['priority', 'code', 'date']).optional(), filterPriority: PriorityFilter.optional(), filterStatus: z.enum(['ready', 'blocked', 'done', 'all']).optional(), sortDir: SortDir.optional(), }).strict() const StoryPanelPrefs = z.object({ sort: z.enum(['priority', 'code', 'date']).optional(), }).strict() const JobsColumnPrefs = z.object({ kinds: z.array(z.string()), statuses: z.array(z.string()), }).strict() const JobsViewPrefs = z.object({ timeFilter: z.enum(JOBS_TIME_FILTER_VALUES).optional(), }).strict() const IdeasListPrefs = z.object({ filterStatuses: z.array( z.enum(IDEA_STATUS_API_VALUES as [IdeaStatusApi, ...IdeaStatusApi[]]) ).optional(), }).strict() const ViewsPrefs = z.object({ sprintBacklog: SprintBacklogPrefs.optional(), pbiList: PbiListPrefs.optional(), storyPanel: StoryPanelPrefs.optional(), jobsColumns: z.record(z.string(), JobsColumnPrefs).optional(), jobs: JobsViewPrefs.optional(), ideasList: IdeasListPrefs.optional(), }).strict() const DevToolsPrefs = z.object({ debugMode: z.boolean().optional(), }).strict() const LayoutPrefs = z.object({ splitPanePositions: z.record(z.string(), z.array(z.number())).optional(), activeSprints: z.record(z.string(), z.string().nullable()).optional(), activePbis: z.record(z.string(), z.string().nullable()).optional(), activeStories: z.record(z.string(), z.string().nullable()).optional(), }).strict() const PbiIntent = z.enum(['all', 'none']) const StoryOverrides = z.object({ add: z.array(z.string()), remove: z.array(z.string()), }).strict() const PendingSprintDraftSchema = z.object({ goal: z.string().min(1), startAt: z.string().date().optional(), endAt: z.string().date().optional(), pbiIntent: z.record(z.string(), PbiIntent).default({}), storyOverrides: z.record(z.string(), StoryOverrides).default({}), }).strict() const WorkflowPrefs = z.object({ pendingSprintDraft: z.record(z.string(), PendingSprintDraftSchema).optional(), }).strict() export const UserSettingsSchema = z.object({ views: ViewsPrefs.optional(), devTools: DevToolsPrefs.optional(), layout: LayoutPrefs.optional(), workflow: WorkflowPrefs.optional(), }).strict() export type UserSettings = z.infer export type PendingSprintDraft = z.infer export type PbiIntent = z.infer export type StoryOverrides = z.infer export const DEFAULT_USER_SETTINGS: UserSettings = {} function isPlainObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } export function mergeSettings( prev: UserSettings, patch: Partial, ): UserSettings { const out: Record = { ...prev } for (const [key, patchValue] of Object.entries(patch)) { if (patchValue === undefined) continue const prevValue = (prev as Record)[key] if (isPlainObject(patchValue) && isPlainObject(prevValue)) { out[key] = mergeSettings( prevValue as UserSettings, patchValue as Partial, ) } else { out[key] = patchValue } } return out as UserSettings } export function parseUserSettings(raw: unknown): UserSettings { if (raw === null || raw === undefined) return DEFAULT_USER_SETTINGS const result = UserSettingsSchema.safeParse(raw) return result.success ? result.data : DEFAULT_USER_SETTINGS }