feat: implement AccessDeniedScreen and enhance error handling

- Introduced AccessDeniedScreen component for improved user experience when access is denied, replacing the previous AccessDenied component.
- Updated CurrentDutyView and CalendarPage to handle access denied scenarios, displaying the new screen appropriately.
- Enhanced tests for CurrentDutyView and AccessDeniedScreen to ensure correct rendering and functionality under access denied conditions.
- Refactored localization messages to include new labels for access denied scenarios in both English and Russian.
This commit is contained in:
2026-03-04 17:51:30 +03:00
parent 3244fbe505
commit 33359f589a
12 changed files with 258 additions and 109 deletions

View File

@@ -30,6 +30,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
import type { DutyWithUser } from "@/types";
export interface CurrentDutyViewProps {
@@ -39,7 +40,7 @@ export interface CurrentDutyViewProps {
openedFromPin?: boolean;
}
type ViewState = "loading" | "error" | "ready";
type ViewState = "loading" | "error" | "accessDenied" | "ready";
/**
* Full-screen current duty view with Telegram BackButton and auto-updating remaining time.
@@ -53,6 +54,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
const [state, setState] = useState<ViewState>("loading");
const [duty, setDuty] = useState<DutyWithUser | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [accessDeniedDetail, setAccessDeniedDetail] = useState<string | null>(null);
const [remaining, setRemaining] = useState<{ hours: number; minutes: number } | null>(null);
const loadTodayDuties = useCallback(
@@ -74,14 +76,17 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
}
} catch (e) {
if (signal?.aborted) return;
setState("error");
const msg =
e instanceof AccessDeniedError && e.serverDetail
? e.serverDetail
: t("error_generic");
setErrorMessage(msg);
setDuty(null);
setRemaining(null);
if (e instanceof AccessDeniedError) {
setState("accessDenied");
setAccessDeniedDetail(e.serverDetail ?? null);
setDuty(null);
setRemaining(null);
} else {
setState("error");
setErrorMessage(t("error_generic"));
setDuty(null);
setRemaining(null);
}
}
},
[initDataRaw, lang, t]
@@ -195,6 +200,17 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
);
}
if (state === "accessDenied") {
return (
<AccessDeniedScreen
serverDetail={accessDeniedDetail}
primaryAction="back"
onBack={handlePrimaryAction}
openedFromPin={openedFromPin}
/>
);
}
if (state === "error") {
const handleRetry = () => {
setState("loading");