ringgroups: ring-all and hunt groups, API, page, golden case
/api/ring-groups CRUD with members as ordered memberIds (unique, must
exist), number from the ring-group range, a no-answer destination
(never itself), and delete refused while something points at it. The
generator renders sp-rg-{id} (Dial through sp-rg-member: all at once with
the total ring time, or one at a time with the per-member time, music
instead of ringing on request), adds the number to sp-local-numbers, and
destApp sends ring_group destinations there. Golden case ringgroups.
The DestinationPicker is now generic over targets picked by id and gains
"Voicemail of a user" and "Ring group" (M6-T6 completes it). Caller-name
prefixes (DIDs and ring groups) get a space before the caller's name.
Ring groups page with an ordered member list.
e2e m6-t2 found the new-group form failing on an unset no-answer
destination (fixed: defaults to hang up). It checks ring-all (both ring,
191 answers) and hunt (190 alone for 5 s, then 191) with real calls. The
generator debounce test now waits for the apply instead of a fixed 150 ms.
Task: M6-T2
This commit is contained in:
1 parent
17db68a8c5
commit
f87d4d47dd
35 files changed
+1604
-29
No files matched your search
@@ -12,3 +12,4 @@ export * from './dids.ts';
|
||||
export * from './outbound.ts';
|
||||
export * from './voicemail.ts';
|
||||
export * from './sounds.ts';
|
||||
export * from './ringgroups.ts';
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Ring groups (02 ring_groups, 03 §6.7): one number that rings several people.
|
||||
import { z } from 'zod';
|
||||
import { DestinationSchema, type Destination } from './destination.ts';
|
||||
import { InternalNumber } from './extensions.ts';
|
||||
|
||||
export const RING_STRATEGIES = ['ringall', 'hunt'] as const;
|
||||
|
||||
const RingGroupFields = z.object({
|
||||
number: InternalNumber,
|
||||
name: z.string().trim().min(1, 'Required').max(40),
|
||||
/** ringall: everyone at once; hunt: one at a time, in member order. */
|
||||
strategy: z.enum(RING_STRATEGIES),
|
||||
/** Per member for hunt, total for ringall. */
|
||||
ringSeconds: z.number().int().min(5).max(300),
|
||||
/** Shown before the caller's name, e.g. "Sales: ". Same rules as caller ID names (03 §2). */
|
||||
callerIdNamePrefix: z
|
||||
.string()
|
||||
.max(20)
|
||||
.regex(/^[^"'\\;,()[\]{}$<>|]*$/, 'No quotes, commas, brackets or ; $ < > |')
|
||||
.nullable(),
|
||||
musicInsteadOfRing: z.boolean(),
|
||||
/** Extension ids, in ring order. */
|
||||
memberIds: z
|
||||
.array(z.uuid())
|
||||
.max(50)
|
||||
.refine((ids) => new Set(ids).size === ids.length, 'Someone is listed twice'),
|
||||
noAnswerDestination: DestinationSchema,
|
||||
});
|
||||
|
||||
export const RingGroupCreateSchema = RingGroupFields.partial().required({
|
||||
number: true,
|
||||
name: true,
|
||||
noAnswerDestination: true,
|
||||
});
|
||||
export const RingGroupUpdateSchema = RingGroupFields.partial();
|
||||
export type RingGroupCreate = z.input<typeof RingGroupCreateSchema>;
|
||||
export type RingGroupUpdate = z.input<typeof RingGroupUpdateSchema>;
|
||||
|
||||
export type RingGroupDto = z.output<typeof RingGroupFields> & {
|
||||
id: string;
|
||||
noAnswerDestination: Destination;
|
||||
noAnswerSummary: string;
|
||||
createdAt: string;
|
||||
};
|
||||
Reference in new issue
Block a user