feat: enhance admin and contact components with new functionality

- Updated `AdminPage` to conditionally display duty reassignment instructions based on visible groups, improving user guidance.
- Refactored `AdminDutyList` to streamline the display of duties, enhancing visual clarity and organization.
- Introduced `openPhoneLink` and `triggerHapticLight` functions in `ContactLinks` for improved phone link interaction and haptic feedback.
- Added unit tests for `openPhoneLink` to ensure correct functionality and handling of various phone number formats.
- Enhanced existing tests for `ContactLinks` to verify new phone link behavior, ensuring robust testing coverage.
This commit is contained in:
2026-03-06 13:26:04 +03:00
parent 02a586a1c5
commit 6da6c87d3c
6 changed files with 148 additions and 18 deletions

View File

@@ -0,0 +1,51 @@
/**
* Unit tests for openPhoneLink: creates a temporary tel: link and triggers click.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { openPhoneLink } from "./open-phone-link";
describe("openPhoneLink", () => {
let appendedAnchor: HTMLAnchorElement | null;
let anchorClickSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
appendedAnchor = null;
anchorClickSpy = vi.fn();
vi.spyOn(window, "open").mockReturnValue(null);
vi.spyOn(document.body, "appendChild").mockImplementation((node: Node) => {
const el = node as HTMLAnchorElement;
if (el.tagName === "A" && el.href) {
appendedAnchor = el;
el.click = anchorClickSpy;
}
return node;
});
vi.spyOn(document.body, "removeChild").mockImplementation((node: Node) => node);
});
it("does nothing when phone is null or empty", () => {
openPhoneLink(null);
openPhoneLink("");
openPhoneLink(" ");
expect(appendedAnchor).toBeNull();
expect(anchorClickSpy).not.toHaveBeenCalled();
});
it("creates anchor with tel URL and triggers click for valid phone", () => {
openPhoneLink("+79991234567");
expect(appendedAnchor).not.toBeNull();
expect(appendedAnchor!.href).toMatch(/tel:\+79991234567$/);
expect(anchorClickSpy).toHaveBeenCalled();
});
it("builds correct tel URL for 10-digit Russian number", () => {
openPhoneLink("9146522209");
expect(appendedAnchor!.href).toMatch(/tel:\+79146522209$/);
});
it("builds correct tel URL for 11-digit number starting with 8", () => {
openPhoneLink("89146522209");
expect(appendedAnchor!.href).toMatch(/tel:\+79146522209$/);
});
});

View File

@@ -0,0 +1,46 @@
/**
* Opens a phone number for calling. Uses native tel: navigation so the OS
* or WebView can open the dialer. Does not use Telegram openLink() for tel:
* (protocol is not supported; it closes the app on desktop and does nothing on mobile).
*/
/**
* Builds a tel: URL from a phone string (digits and optional leading + only).
*/
function buildTelUrl(phone: string): string {
const trimmed = String(phone).trim();
if (trimmed === "") return "";
const digits = trimmed.replace(/\D/g, "");
if (digits.length === 11 && (digits[0] === "7" || digits[0] === "8")) {
return `tel:+7${digits.slice(1)}`;
}
if (digits.length === 10) {
return `tel:+7${digits}`;
}
return `tel:${trimmed.startsWith("+") ? "+" : ""}${digits}`;
}
/**
* Opens the given phone number for calling. Tries window.open(telUrl) first
* (reported to work for tel: in some Telegram WebViews), then a programmatic
* click on a temporary tel: link. openLink(tel:) is not used — Telegram does
* not support it (closes app on desktop, no dialer on mobile). Safe to call
* from click handlers; does not throw.
*/
export function openPhoneLink(phone: string | null | undefined): void {
if (phone == null || String(phone).trim() === "") return;
const telUrl = buildTelUrl(phone);
if (telUrl === "") return;
if (typeof window === "undefined" || !window.document) return;
const opened = window.open(telUrl);
if (opened !== null) return;
const anchor = window.document.createElement("a");
anchor.href = telUrl;
anchor.style.display = "none";
anchor.setAttribute("aria-hidden", "true");
window.document.body.appendChild(anchor);
anchor.click();
anchor.remove();
}