feat: enhance admin page and testing functionality

- Updated admin page to include navigation buttons for month selection, improving user experience.
- Refactored `AdminDutyList` to group duties by date, enhancing the display and organization of duties.
- Improved error handling in `ReassignSheet` by using i18n keys for error messages, ensuring better localization support.
- Enhanced tests for admin page and components to reflect recent changes, ensuring accuracy in functionality and accessibility.
- Added event dispatch for configuration loading in the app configuration, improving integration with the Telegram Mini App.
This commit is contained in:
2026-03-06 12:04:16 +03:00
parent 53a899ea26
commit 02a586a1c5
11 changed files with 324 additions and 113 deletions

View File

@@ -55,7 +55,7 @@ function mockFetchForAdmin(
const adminMe = options?.adminMe ?? { is_admin: true };
vi.stubGlobal(
"fetch",
vi.fn((url: string, init?: RequestInit) => {
vi.fn((url: string, _init?: RequestInit) => {
if (url.includes("/api/admin/me")) {
return Promise.resolve({
ok: true,
@@ -163,7 +163,7 @@ describe("AdminPage", () => {
fireEvent.click(dutyButton);
await waitFor(() => {
expect(
screen.getByLabelText(/select user|выберите пользователя/i)
screen.getByRole("radiogroup", { name: /select user|выберите пользователя/i })
).toBeInTheDocument();
});
expect(screen.getByRole("button", { name: /save|сохранить/i })).toBeInTheDocument();
@@ -231,10 +231,9 @@ describe("AdminPage", () => {
});
fireEvent.click(screen.getByRole("button", { name: /Alice/ }));
await waitFor(() => {
expect(screen.getByLabelText(/select user|выберите пользователя/i)).toBeInTheDocument();
expect(screen.getByRole("radiogroup", { name: /select user|выберите пользователя/i })).toBeInTheDocument();
});
const select = screen.getByLabelText(/select user|выберите пользователя/i);
fireEvent.change(select, { target: { value: "2" } });
fireEvent.click(screen.getByRole("radio", { name: /Bob/ }));
fireEvent.click(screen.getByRole("button", { name: /save|сохранить/i }));
await waitFor(() => {
expect(

View File

@@ -11,6 +11,8 @@ import { useTranslation } from "@/i18n/use-translation";
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
import { LoadingState } from "@/components/states/LoadingState";
import { ErrorState } from "@/components/states/ErrorState";
import { Button } from "@/components/ui/button";
import { ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon } from "lucide-react";
const PAGE_WRAPPER_CLASS =
"content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6";
@@ -50,27 +52,54 @@ export default function AdminPage() {
);
}
const month = admin.currentMonth.getMonth();
const year = admin.currentMonth.getFullYear();
const month = admin.adminMonth.getMonth();
const year = admin.adminMonth.getFullYear();
return (
<div className={PAGE_WRAPPER_CLASS}>
<header className="sticky top-0 z-10 flex flex-col items-center border-b bg-[var(--header-bg)] py-3">
<h1
className="m-0 flex flex-col items-center justify-center gap-0 leading-none"
aria-label={`${t("admin.title")}, ${monthName(month)} ${year}`}
>
<span className="text-xs font-normal leading-none text-muted">
{year}
</span>
<span className="text-[1.1rem] font-semibold leading-tight sm:text-[1.25rem]">
{monthName(month)}
</span>
</h1>
<div className="flex w-full items-center justify-between px-1">
<Button
type="button"
variant="secondary"
size="icon"
className="size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"
aria-label={t("nav.prev_month")}
disabled={admin.loading}
onClick={admin.onPrevMonth}
>
<ChevronLeftIcon className="size-5" aria-hidden />
</Button>
<h1
className="m-0 flex flex-col items-center justify-center gap-0 leading-none"
aria-label={`${t("admin.title")}, ${monthName(month)} ${year}`}
>
<span className="text-xs font-normal leading-none text-muted">
{year}
</span>
<span className="text-[1.1rem] font-semibold leading-tight sm:text-[1.25rem]">
{monthName(month)}
</span>
</h1>
<Button
type="button"
variant="secondary"
size="icon"
className="size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"
aria-label={t("nav.next_month")}
disabled={admin.loading}
onClick={admin.onNextMonth}
>
<ChevronRightIcon className="size-5" aria-hidden />
</Button>
</div>
<p className="text-sm text-muted-foreground m-0 mt-1">
{admin.loading ? "…" : t("admin.duties_count", { count: String(admin.dutyOnly.length) })}
</p>
</header>
{admin.successMessage && (
<p className="mt-3 text-sm text-[var(--duty)]" role="status">
<p className="mt-3 text-sm text-[var(--duty)]" role="status" aria-live="polite">
{admin.successMessage}
</p>
)}
@@ -96,7 +125,7 @@ export default function AdminPage() {
{t("admin.reassign_duty")}: {t("admin.select_user")}
</p>
<AdminDutyList
duties={admin.visibleDuties}
groups={admin.visibleGroups}
hasMore={admin.hasMore}
sentinelRef={admin.sentinelRef}
onSelectDuty={admin.openReassign}
@@ -112,7 +141,7 @@ export default function AdminPage() {
setSelectedUserId={admin.setSelectedUserId}
users={admin.usersForSelect}
saving={admin.saving}
reassignError={admin.reassignError}
reassignErrorKey={admin.reassignErrorKey}
onReassign={admin.handleReassign}
onRequestClose={admin.requestCloseSheet}
onCloseAnimationEnd={admin.closeReassign}

View File

@@ -50,12 +50,11 @@ describe("Page", () => {
});
it("sets document title for ru when store lang is ru", async () => {
(globalThis.window as unknown as { __DT_LANG?: string }).__DT_LANG = "ru";
useAppStore.getState().setLang("ru");
render(<Page />);
await screen.findByRole("grid", { name: "Calendar" });
await waitFor(() => {
expect(document.title).toBe("Календарь дежурств");
});
expect(document.title).toBe("Календарь дежурств");
expect(document.documentElement.lang).toBe("ru");
});
it("renders AccessDeniedScreen when not allowed and delay has passed", async () => {