Files
duty-teller/webapp-next/src/store/app-store.ts
Nikolay Tatarinov c390a4dd6e feat: implement admin panel functionality in Mini App
- Added new API endpoints for admin features: `GET /api/admin/me`, `GET /api/admin/users`, and `PATCH /api/admin/duties/:id` to manage user duties.
- Introduced `UserForAdmin` and `AdminDutyReassignBody` schemas for handling admin-related data.
- Updated documentation to include Mini App design guidelines and admin panel functionalities.
- Enhanced tests for admin API to ensure proper access control and functionality.
- Improved error handling and localization for admin actions.
2026-03-06 09:57:26 +03:00

101 lines
3.8 KiB
TypeScript

/**
* 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<Pick<AppState, "currentMonth" | "pendingMonth" | "lang" | "duties" | "calendarEvents" | "dataForMonthKey" | "loading" | "error" | "accessDenied" | "accessDeniedDetail" | "currentView" | "selectedDay" | "appContentReady" | "isAdmin">>) => 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<AppState>((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),
}));