lct-51: голосовой маршрут ДДС — вышестоящий начальник через WebRTC 6103 с записью

This commit is contained in:
Ivan Gerasimov 2026-09-27 23:41:15 +03:00
commit bfaadd556c
9 changed files with 63 additions and 12 deletions

View file

@ -12,12 +12,19 @@ const LABEL: Record<SipStatus, string> = {
error: "ошибка",
};
/** Абонент быстрого набора: пульт ДДС звонит бригаде и начальнику одной кнопкой. */
export interface SipContact {
label: string;
number: string;
}
export function SipPhone({
defaultExtension = "6101", defaultTarget = "6102", title = "Локальный SIP/WebRTC",
defaultExtension = "6101", defaultTarget = "6102", title = "Локальный SIP/WebRTC", contacts = [],
}: {
defaultExtension?: string;
defaultTarget?: string;
title?: string;
contacts?: SipContact[];
}) {
const phone = useSipPhone();
const [extension, setExtension] = useState(defaultExtension);
@ -46,6 +53,10 @@ export function SipPhone({
<label>Номер собеседника<input aria-label="Номер SIP собеседника" inputMode="numeric" value={target}
disabled={inCall} onChange={(event) => setTarget(event.target.value)} /></label>
{phone.status === "registered" && <button type="button" onClick={() => phone.dial(target)}>Позвонить</button>}
{phone.status === "registered" && contacts.map((contact) => <button type="button" key={contact.number}
onClick={() => { setTarget(contact.number); phone.dial(contact.number); }}>
Позвонить: {contact.label} · {contact.number}
</button>)}
{phone.status === "incoming" && <button type="button" className="sip-answer" onClick={phone.answer}>Ответить · {phone.peer}</button>}
{inCall && phone.status !== "incoming" && <span>{phone.peer || target}</span>}
{inCall && <button type="button" className="sip-hangup" onClick={phone.hangup}>Завершить</button>}

View file

@ -604,9 +604,11 @@ export function Dds() {
</div><aside className="dds-right">
{pane === "phone" && <section className="dds-panel dds-phone"><div className="dds-panel-title"><h2>IP-телефон</h2><span>SIP/WebRTC + учебный протокол</span></div>
<Suspense fallback={<p className="dds-hint">Загружаем IP-телефон…</p>}>
<SipPhone defaultExtension="6101" defaultTarget="6102" title="Реальный локальный канал ДДС" />
<SipPhone defaultExtension="6101" defaultTarget="6102" title="Реальный локальный канал ДДС"
contacts={[{ label: "старший группы", number: "6102" },
{ label: "вышестоящий начальник", number: "6103" }]} />
</Suspense>
<p className="dds-hint">На втором рабочем месте откройте <a href="/phone" target="_blank" rel="noreferrer">телефон старшего группы</a> и зарегистрируйте 6102. Пароли 6101/6102 выдаёт локальный администратор командой <code>make sip-credentials</code>.</p>
<p className="dds-hint">На втором рабочем месте откройте <a href="/phone" target="_blank" rel="noreferrer">телефон старшего группы</a> (6102); преподаватель в роли вышестоящего начальника — <a href="/phone?role=superior" target="_blank" rel="noreferrer">телефон 6103</a>. Пароли 6101–6103 выдаёт локальный администратор командой <code>make sip-credentials</code>.</p>
<h3 className="dds-protocol-title">Учебный протокол доклада</h3>
<p className="dds-hint">Связь используется при необходимости. Разговор и учебный доклад сохраняются в журнале, но не блокируют изменение статуса.</p>
<label htmlFor="dds-crew">Дежурная бригада</label>

View file

@ -1,9 +1,22 @@
import { SipPhone } from "@/features/sip/SipPhone";
// Второе рабочее место голосовой связи ДДС. Старший группы — по умолчанию;
// вышестоящего начальника (?role=superior) играет преподаватель: это
// обязательный маршрут по ответу заказчика (П.7).
const ROLES = {
crew: { title: "Телефон старшего группы", extension: "6102", heading: "IP-телефон старшего группы" },
superior: { title: "Телефон вышестоящего начальника", extension: "6103", heading: "IP-телефон вышестоящего начальника" },
} as const;
export function Phone() {
const role = new URLSearchParams(window.location.search).get("role") === "superior" ? "superior" : "crew";
const { title, extension, heading } = ROLES[role];
return <main className="page phone-page">
<h1>IP-телефон старшего группы</h1>
<p>Откройте этот экран на втором рабочем месте, зарегистрируйте номер 6102 и примите вызов ДДС 6101.</p>
<SipPhone defaultExtension="6102" defaultTarget="6101" title="Телефон старшего группы" />
<h1>{heading}</h1>
<p>Откройте этот экран на втором рабочем месте, зарегистрируйте номер {extension} и примите вызов ДДС 6101.</p>
<SipPhone key={role} defaultExtension={extension} defaultTarget="6101" title={title} />
<p className="ref">{role === "crew"
? <>Роль вышестоящего начальника — <a href="/phone?role=superior">телефон 6103</a>.</>
: <>Роль старшего группы — <a href="/phone">телефон 6102</a>.</>}</p>
</main>;
}