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
@@ -13,3 +13,4 @@ export * from './outbound.ts';
|
||||
export * from './voicemail.ts';
|
||||
export * from './sounds.ts';
|
||||
export * from './ringgroups.ts';
|
||||
export * from './ivrs.ts';
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Digital receptionist / IVR (02 ivrs, ivr_options, 03 §6.9): "press 1 for sales…".
|
||||
import { z } from 'zod';
|
||||
import { DestinationSchema, type Destination } from './destination.ts';
|
||||
import { InternalNumber } from './extensions.ts';
|
||||
|
||||
export const IVR_DIGITS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'] as const;
|
||||
|
||||
const IvrFields = z.object({
|
||||
number: InternalNumber,
|
||||
name: z.string().trim().min(1, 'Required').max(40),
|
||||
greetingSoundId: z.uuid('Pick a greeting'),
|
||||
/** null = the stock "that's not a valid choice" (pbx-invalid). */
|
||||
invalidSoundId: z.uuid().nullable(),
|
||||
timeoutSeconds: z.number().int().min(1).max(30),
|
||||
maxRetries: z.number().int().min(1).max(10),
|
||||
/** Callers may dial an internal number (e.g. 101) instead of pressing an option. */
|
||||
allowDirectDial: z.boolean(),
|
||||
options: z
|
||||
.array(z.object({ digit: z.enum(IVR_DIGITS), destination: DestinationSchema }))
|
||||
.max(12)
|
||||
.refine((o) => new Set(o.map((x) => x.digit)).size === o.length, 'A key is used twice'),
|
||||
timeoutDestination: DestinationSchema,
|
||||
invalidDestination: DestinationSchema,
|
||||
});
|
||||
|
||||
export const IvrCreateSchema = IvrFields.partial().required({
|
||||
number: true,
|
||||
name: true,
|
||||
greetingSoundId: true,
|
||||
timeoutDestination: true,
|
||||
invalidDestination: true,
|
||||
});
|
||||
export const IvrUpdateSchema = IvrFields.partial();
|
||||
export type IvrCreate = z.input<typeof IvrCreateSchema>;
|
||||
export type IvrUpdate = z.input<typeof IvrUpdateSchema>;
|
||||
|
||||
export type IvrDto = Omit<z.output<typeof IvrFields>, 'options'> & {
|
||||
id: string;
|
||||
options: { digit: (typeof IVR_DIGITS)[number]; destination: Destination; summary: string }[];
|
||||
createdAt: string;
|
||||
};
|
||||
Reference in new issue
Block a user