diff --git a/apps/api/src/services/destinations.ts b/apps/api/src/services/destinations.ts index 262c3bd..05209d8 100644 --- a/apps/api/src/services/destinations.ts +++ b/apps/api/src/services/destinations.ts @@ -40,8 +40,10 @@ async function targetName(db: Db, type: RefType, id: string): Promise { destination: { type: 'ivr', id: ivrId }, }) ).json(); - expect(did.destinationSummary).toBe('Digital receptionist Main menu'); + expect(did.destinationSummary).toBe('Digital receptionist Main menu (600)'); expect((await req('DELETE', `/api/ivrs/${ivrId}`)).statusCode).toBe(409); // The refused delete must not have dropped its options. expect((await req('GET', `/api/ivrs/${ivrId}`)).json().options).toHaveLength(1); diff --git a/apps/api/test/ringgroups.test.ts b/apps/api/test/ringgroups.test.ts index 9ec62fd..363b445 100644 --- a/apps/api/test/ringgroups.test.ts +++ b/apps/api/test/ringgroups.test.ts @@ -113,7 +113,7 @@ describe.skipIf(!hasTestDb)('/api/ring-groups', () => { trunkId, destination: { type: 'ring_group', id: groupId }, }); - expect(did.json().destinationSummary).toBe('Ring group Sales'); + expect(did.json().destinationSummary).toBe('Ring group Sales (500)'); expect(await dialplan()).toContain(`Goto(sp-rg-${groupId},s,1)`); const busy = await req('DELETE', `/api/ring-groups/${groupId}`); expect(busy.statusCode).toBe(409); diff --git a/apps/web/src/components/DestinationPicker.tsx b/apps/web/src/components/DestinationPicker.tsx index 21c0437..b805058 100644 --- a/apps/web/src/components/DestinationPicker.tsx +++ b/apps/web/src/components/DestinationPicker.tsx @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // "Where should the call go?" (05 §3): a type, then the target (or a number). -// Types join as their features land; M6-T6 finishes the list. +// Every type that exists (M6-T6); queues and conferences join with M8. import { useQuery } from '@tanstack/react-query'; import { useId, useState } from 'react'; import { normalizeNumber, type Destination } from '@selfphoneng/shared'; diff --git a/apps/web/src/pages/SetupPage.tsx b/apps/web/src/pages/SetupPage.tsx index 51096e4..c48db5f 100644 --- a/apps/web/src/pages/SetupPage.tsx +++ b/apps/web/src/pages/SetupPage.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // First-run setup wizard (05 §2.1). Every step saves before moving on; some can be skipped. // M3 also brings steps 7 (users) and 8 (main number routing) because "calls work using only the -// wizard" needs them; step 8's ring group and IVR choices arrive with M6. +// wizard" needs them; M6 adds "Ring everyone" to step 8 (an IVR needs a recorded greeting first). import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState, type ReactNode } from 'react'; import { useNavigate, useSearchParams } from 'react-router'; @@ -9,6 +9,7 @@ import { formatNumber, type Destination, type TrunkDto } from '@selfphoneng/shar import { didsApi, extensionsApi, + ringGroupsApi, settingsApi, systemApi, trunksApi, @@ -619,6 +620,26 @@ function RoutingStep({ next }: { next: () => void }) { const qc = useQueryClient(); const dids = useQuery({ queryKey: ['dids'], queryFn: didsApi.list }); const [dest, setDest] = useState>({}); + /** A ring group "Everyone" with every extension (made once, kept up to date). */ + const everyone = useMutation({ + mutationFn: async () => { + const exts = (await extensionsApi.list()).sort((a, b) => Number(a.number) - Number(b.number)); + if (!exts.length) throw new Error('Add users with extensions first (step 7)'); + const memberIds = exts.map((e) => e.id); + const existing = (await ringGroupsApi.list()).find((g) => g.name === 'Everyone'); + const g = existing + ? await ringGroupsApi.update(existing.id, { memberIds }) + : await ringGroupsApi.create({ + number: (await extensionsApi.nextNumber('ringGroup')) ?? '500', + name: 'Everyone', + memberIds, + // Nobody picks up: the first person's voicemail rather than a dead end. + noAnswerDestination: { type: 'voicemail', id: exts[0]!.id }, + }); + await qc.invalidateQueries({ queryKey: ['ring-groups'] }); + return g; + }, + }); const save = useMutation({ mutationFn: async () => { for (const [id, d] of Object.entries(dest)) @@ -640,14 +661,38 @@ function RoutingStep({ next }: { next: () => void }) { const incomplete = Object.values(dest).some((d) => d === null); return ( <> - {dids.data.map((d) => ( - setDest({ ...dest, [d.id]: v })} - /> - ))} +

+ A digital receptionist ("press 1 for sales…") needs a recorded greeting: build it under + Digital receptionist later and pick it here or on the Phone numbers page. +

+ {dids.data.map((d) => { + const value = dest[d.id] ?? d.destination; + return ( +
+ setDest({ ...dest, [d.id]: v })} + /> +
+ +
+
+ ); + })} + {everyone.isError && {everyone.error.message}} {save.isError && {save.error.message}}