ivrs: digital receptionist with keypad grid, SoundPicker, golden case
/api/ivrs CRUD: greeting and wrong-key sounds must be prompts, every
option destination must exist (an option may point at its own menu),
keys unique; delete drops the menu's own options before checking
references, inside a transaction so a refused delete keeps them. The
generator renders sp-ivr-{id} per 03 §6.9 (Background greeting,
WaitExten, keys sorted, i/t retry then give up, include sp-local-numbers
for direct dial), adds the number to sp-local-numbers, and destApp sends
ivr destinations there. Golden case ivr (incl. # and self-reference).
Web: Digital receptionist page with a 3x4 keypad grid (pick a key, then
its destination or "unused"), SoundPicker (pick, play, upload new right
in the form), DestinationPicker gains "Digital receptionist" and a shared
target source; useDescribeDestination labels unsaved keys.
e2e m6-t3: menu built in the UI, the greeting plays, keys 1/2 ring the
right phones, a wrong key plays pbx-invalid and repeats the greeting.
Task: M6-T3
This commit is contained in:
1 parent
f87d4d47dd
commit
15624f7b77
35 files changed
+1730
-16
No files matched your search
@@ -6,9 +6,12 @@ import type { Db } from '../db/client.ts';
|
||||
import {
|
||||
dids,
|
||||
extensions,
|
||||
ivrOptions,
|
||||
ivrs,
|
||||
outboundRoutes,
|
||||
ringGroupMembers,
|
||||
ringGroups,
|
||||
soundFiles,
|
||||
trunks,
|
||||
users,
|
||||
} from '../db/schema.ts';
|
||||
@@ -86,6 +89,21 @@ export type SnapRingGroup = {
|
||||
noAnswerDestination: Destination;
|
||||
};
|
||||
|
||||
export type SnapIvr = {
|
||||
id: string;
|
||||
number: string;
|
||||
name: string;
|
||||
/** Asterisk sound names, e.g. custom/<id>. */
|
||||
greeting: string;
|
||||
invalidSound: string | null;
|
||||
timeoutSeconds: number;
|
||||
maxRetries: number;
|
||||
allowDirectDial: boolean;
|
||||
options: { digit: string; destination: Destination }[];
|
||||
timeoutDestination: Destination;
|
||||
invalidDestination: Destination;
|
||||
};
|
||||
|
||||
export type Snapshot = {
|
||||
/** "YYYY-MM-DD" in general.timezone; render() never reads the clock. */
|
||||
today: string;
|
||||
@@ -95,6 +113,7 @@ export type Snapshot = {
|
||||
trunks: SnapTrunk[];
|
||||
routes: SnapRoute[];
|
||||
ringGroups: SnapRingGroup[];
|
||||
ivrs: SnapIvr[];
|
||||
};
|
||||
|
||||
export function todayIn(timezone: string, now = new Date()): string {
|
||||
@@ -131,6 +150,13 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
const trunkRows = await db.select().from(trunks);
|
||||
const routeRows = await db.select().from(outboundRoutes);
|
||||
const groupRows = await db.select().from(ringGroups);
|
||||
const ivrRows = await db.select().from(ivrs);
|
||||
const optionRows = await db.select().from(ivrOptions);
|
||||
const soundName = new Map(
|
||||
(await db.select({ id: soundFiles.id, name: soundFiles.asteriskName }).from(soundFiles)).map(
|
||||
(s) => [s.id, s.name],
|
||||
),
|
||||
);
|
||||
const memberRows = await db
|
||||
.select({ groupId: ringGroupMembers.ringGroupId, number: extensions.number })
|
||||
.from(ringGroupMembers)
|
||||
@@ -141,6 +167,21 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
today: todayIn(settings['general.timezone'], now),
|
||||
settings,
|
||||
dids: didRows.map((d) => ({ ...d, destination: d.destination as Destination })),
|
||||
ivrs: ivrRows.map((r) => ({
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
name: r.name,
|
||||
greeting: soundName.get(r.greetingSoundId)!,
|
||||
invalidSound: r.invalidSoundId ? (soundName.get(r.invalidSoundId) ?? null) : null,
|
||||
timeoutSeconds: r.timeoutSeconds,
|
||||
maxRetries: r.maxRetries,
|
||||
allowDirectDial: r.allowDirectDial,
|
||||
options: optionRows
|
||||
.filter((o) => o.ivrId === r.id)
|
||||
.map((o) => ({ digit: o.digit, destination: o.destination as Destination })),
|
||||
timeoutDestination: r.timeoutDestination as Destination,
|
||||
invalidDestination: r.invalidDestination as Destination,
|
||||
})),
|
||||
ringGroups: groupRows.map((g) => ({
|
||||
id: g.id,
|
||||
number: g.number,
|
||||
|
||||
Reference in new issue
Block a user