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
@@ -1,9 +1,17 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Everything render() needs, loaded in one go (03 §1). render() itself does no I/O.
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { asc, eq } from 'drizzle-orm';
|
||||
import { alias } from 'drizzle-orm/pg-core';
|
||||
import type { Db } from '../db/client.ts';
|
||||
import { dids, extensions, outboundRoutes, trunks, users } from '../db/schema.ts';
|
||||
import {
|
||||
dids,
|
||||
extensions,
|
||||
outboundRoutes,
|
||||
ringGroupMembers,
|
||||
ringGroups,
|
||||
trunks,
|
||||
users,
|
||||
} from '../db/schema.ts';
|
||||
import { decrypt } from '../crypto/secrets.ts';
|
||||
import { getAllSettings } from '../services/settings.ts';
|
||||
import type { Destination, OutboundClass, RoutePattern } from '@selfphoneng/shared';
|
||||
@@ -65,6 +73,19 @@ export type SnapTrunk = {
|
||||
maxChannels: number | null;
|
||||
};
|
||||
|
||||
export type SnapRingGroup = {
|
||||
id: string;
|
||||
number: string;
|
||||
name: string;
|
||||
strategy: 'ringall' | 'hunt';
|
||||
ringSeconds: number;
|
||||
callerIdNamePrefix: string | null;
|
||||
musicInsteadOfRing: boolean;
|
||||
/** Extension numbers in ring order. */
|
||||
members: string[];
|
||||
noAnswerDestination: Destination;
|
||||
};
|
||||
|
||||
export type Snapshot = {
|
||||
/** "YYYY-MM-DD" in general.timezone; render() never reads the clock. */
|
||||
today: string;
|
||||
@@ -73,6 +94,7 @@ export type Snapshot = {
|
||||
dids: SnapDid[];
|
||||
trunks: SnapTrunk[];
|
||||
routes: SnapRoute[];
|
||||
ringGroups: SnapRingGroup[];
|
||||
};
|
||||
|
||||
export function todayIn(timezone: string, now = new Date()): string {
|
||||
@@ -108,11 +130,28 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
.from(dids);
|
||||
const trunkRows = await db.select().from(trunks);
|
||||
const routeRows = await db.select().from(outboundRoutes);
|
||||
const groupRows = await db.select().from(ringGroups);
|
||||
const memberRows = await db
|
||||
.select({ groupId: ringGroupMembers.ringGroupId, number: extensions.number })
|
||||
.from(ringGroupMembers)
|
||||
.innerJoin(extensions, eq(extensions.id, ringGroupMembers.extensionId))
|
||||
.orderBy(asc(ringGroupMembers.position));
|
||||
const didNumber = new Map(didRows.map((d) => [d.id, d.number]));
|
||||
return {
|
||||
today: todayIn(settings['general.timezone'], now),
|
||||
settings,
|
||||
dids: didRows.map((d) => ({ ...d, destination: d.destination as Destination })),
|
||||
ringGroups: groupRows.map((g) => ({
|
||||
id: g.id,
|
||||
number: g.number,
|
||||
name: g.name,
|
||||
strategy: g.strategy,
|
||||
ringSeconds: g.ringSeconds,
|
||||
callerIdNamePrefix: g.callerIdNamePrefix,
|
||||
musicInsteadOfRing: g.musicInsteadOfRing,
|
||||
members: memberRows.filter((m) => m.groupId === g.id).map((m) => m.number),
|
||||
noAnswerDestination: g.noAnswerDestination as Destination,
|
||||
})),
|
||||
routes: routeRows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
|
||||
Reference in new issue
Block a user