/** * Global application state (Zustand). * Replaces the mutable state from webapp/js/dom.js. */ import { create } from "zustand"; import type { DutyWithUser, CalendarEvent } from "@/types"; import { getStartParamFromUrl } from "@/lib/launch-params"; export type CurrentView = "calendar" | "currentDuty"; /** YYYY-MM key for the month that duties/calendarEvents belong to; null when none loaded. */ export type DataForMonthKey = string | null; export interface AppState { currentMonth: Date; /** When set, we are loading this month; currentMonth and data stay until load completes. */ pendingMonth: Date | null; lang: "ru" | "en"; duties: DutyWithUser[]; calendarEvents: CalendarEvent[]; /** YYYY-MM: duties and calendarEvents are for this month; null when loading or no data. */ dataForMonthKey: DataForMonthKey; loading: boolean; error: string | null; accessDenied: boolean; /** Server detail from API 403 response; shown in AccessDeniedScreen. */ accessDeniedDetail: string | null; currentView: CurrentView; selectedDay: string | null; /** True when the first visible screen has finished loading; used to hide content until ready(). */ appContentReady: boolean; /** True when GET /api/admin/me returned is_admin: true; used to show Admin link. */ isAdmin: boolean; setCurrentMonth: (d: Date) => void; nextMonth: () => void; prevMonth: () => void; setDuties: (d: DutyWithUser[]) => void; setCalendarEvents: (e: CalendarEvent[]) => void; setLoading: (v: boolean) => void; setError: (msg: string | null) => void; setAccessDenied: (v: boolean) => void; setAccessDeniedDetail: (v: string | null) => void; setLang: (v: "ru" | "en") => void; setCurrentView: (v: CurrentView) => void; setSelectedDay: (key: string | null) => void; setAppContentReady: (v: boolean) => void; setIsAdmin: (v: boolean) => void; /** Batch multiple state updates into a single re-render. */ batchUpdate: (partial: Partial>) => void; } const now = new Date(); const initialMonth = new Date(now.getFullYear(), now.getMonth(), 1); /** Initial view: currentDuty when opened via deep link (startParam=duty), else calendar. */ function getInitialView(): CurrentView { if (typeof window === "undefined") return "calendar"; return getStartParamFromUrl() === "duty" ? "currentDuty" : "calendar"; } export const useAppStore = create((set) => ({ currentMonth: initialMonth, pendingMonth: null, lang: "en", duties: [], calendarEvents: [], dataForMonthKey: null, loading: false, error: null, accessDenied: false, accessDeniedDetail: null, currentView: getInitialView(), selectedDay: null, appContentReady: false, isAdmin: false, setCurrentMonth: (d) => set({ currentMonth: d }), nextMonth: () => set((s) => ({ pendingMonth: new Date(s.currentMonth.getFullYear(), s.currentMonth.getMonth() + 1, 1), })), prevMonth: () => set((s) => ({ pendingMonth: new Date(s.currentMonth.getFullYear(), s.currentMonth.getMonth() - 1, 1), })), setDuties: (d) => set({ duties: d }), setCalendarEvents: (e) => set({ calendarEvents: e }), setLoading: (v) => set({ loading: v }), setError: (msg) => set({ error: msg }), setAccessDenied: (v) => set({ accessDenied: v }), setAccessDeniedDetail: (v) => set({ accessDeniedDetail: v }), setLang: (v) => set({ lang: v }), setCurrentView: (v) => set({ currentView: v }), setSelectedDay: (key) => set({ selectedDay: key }), setAppContentReady: (v) => set({ appContentReady: v }), setIsAdmin: (v) => set({ isAdmin: v }), batchUpdate: (partial) => set(partial), }));