diff --git a/webapp-next/src/components/current-duty/CurrentDutyView.test.tsx b/webapp-next/src/components/current-duty/CurrentDutyView.test.tsx
index ee49c7f..aab8314 100644
--- a/webapp-next/src/components/current-duty/CurrentDutyView.test.tsx
+++ b/webapp-next/src/components/current-duty/CurrentDutyView.test.tsx
@@ -57,6 +57,18 @@ describe("CurrentDutyView", () => {
expect(screen.queryByText(/Back to calendar|Назад к календарю/i)).not.toBeInTheDocument();
});
+ it("shows Open calendar button in no-duty view and it calls onBack", async () => {
+ const onBack = vi.fn();
+ render();
+ await screen.findByText(/No one is on duty|Сейчас никто не дежурит/i, {}, { timeout: 3000 });
+ const openCalendarBtn = screen.getByRole("button", {
+ name: /Open calendar|Открыть календарь/i,
+ });
+ expect(openCalendarBtn).toBeInTheDocument();
+ fireEvent.click(openCalendarBtn);
+ expect(onBack).toHaveBeenCalled();
+ });
+
it("shows contact info not set when duty has no phone or username", async () => {
const { fetchDuties } = await import("@/lib/api");
const now = new Date();
@@ -81,4 +93,38 @@ describe("CurrentDutyView", () => {
).toBeInTheDocument();
vi.mocked(fetchDuties).mockResolvedValue([]);
});
+
+ it("shows ends_at line when duty is active", async () => {
+ const { fetchDuties } = await import("@/lib/api");
+ const now = new Date();
+ const start = new Date(now.getTime() - 60 * 60 * 1000);
+ const end = new Date(now.getTime() + 2 * 60 * 60 * 1000);
+ const duty = {
+ id: 1,
+ user_id: 1,
+ start_at: start.toISOString(),
+ end_at: end.toISOString(),
+ event_type: "duty" as const,
+ full_name: "Test User",
+ phone: null,
+ username: null,
+ };
+ vi.mocked(fetchDuties).mockResolvedValue([duty]);
+ render();
+ await screen.findByText("Test User", {}, { timeout: 3000 });
+ expect(
+ screen.getByText(/Until end of shift at|До конца смены в/i)
+ ).toBeInTheDocument();
+ vi.mocked(fetchDuties).mockResolvedValue([]);
+ });
+
+ it("error state shows Retry as first button", async () => {
+ const { fetchDuties } = await import("@/lib/api");
+ vi.mocked(fetchDuties).mockRejectedValue(new Error("Network error"));
+ render();
+ await screen.findByText(/Could not load|Не удалось загрузить/i, {}, { timeout: 3000 });
+ const buttons = screen.getAllByRole("button");
+ expect(buttons[0]).toHaveAccessibleName(/Retry|Повторить/i);
+ vi.mocked(fetchDuties).mockResolvedValue([]);
+ });
});
diff --git a/webapp-next/src/components/current-duty/CurrentDutyView.tsx b/webapp-next/src/components/current-duty/CurrentDutyView.tsx
index f043eb6..08ed618 100644
--- a/webapp-next/src/components/current-duty/CurrentDutyView.tsx
+++ b/webapp-next/src/components/current-duty/CurrentDutyView.tsx
@@ -166,15 +166,14 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
aria-label={t("loading")}
>
-
+
-
-
+
+
-
-
-
+
+
@@ -246,10 +245,17 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
{t("current_duty.no_duty")}
-
+
+
@@ -287,41 +293,45 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
role="article"
aria-labelledby="current-duty-title"
>
-
-
-
- {t("current_duty.title")}
-
+
+ {t("current_duty.title")}
-
+
+
+
+ {t("duty.now_on_duty")}
+
{duty.full_name}
- {shiftLabel} {shiftStr}
+ {shiftLabel}
+
+
+ {shiftStr}
{t("current_duty.remaining_label")}
-
+
{remainingValueStr}
+
+ {t("current_duty.ends_at", { time: formatHHMM(duty.end_at) })}
+
{hasContacts ? (
diff --git a/webapp-next/src/i18n/messages.ts b/webapp-next/src/i18n/messages.ts
index 57f97e8..427da3a 100644
--- a/webapp-next/src/i18n/messages.ts
+++ b/webapp-next/src/i18n/messages.ts
@@ -72,6 +72,7 @@ export const MESSAGES: Record> = {
"current_duty.ends_at": "Until end of shift at {time}",
"current_duty.back": "Back to calendar",
"current_duty.close": "Close",
+ "current_duty.open_calendar": "Open calendar",
"current_duty.contact_info_not_set": "Contact info not set",
"error_boundary.message": "Something went wrong.",
"error_boundary.description": "An unexpected error occurred. Try reloading the app.",
@@ -147,6 +148,7 @@ export const MESSAGES: Record> = {
"current_duty.ends_at": "До конца смены в {time}",
"current_duty.back": "Назад к календарю",
"current_duty.close": "Закрыть",
+ "current_duty.open_calendar": "Открыть календарь",
"current_duty.contact_info_not_set": "Контактные данные не указаны",
"error_boundary.message": "Что-то пошло не так.",
"error_boundary.description": "Произошла непредвиденная ошибка. Попробуйте перезагрузить приложение.",