- 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.
101 lines
3.8 KiB
TypeScript
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),
|
|
}));
|