@visns-studio/visns-components 6.25.0 → 6.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/callQueue/CallQueueDiagnostics.jsx +1043 -0
- package/src/components/callQueue/CallQueuePop.jsx +713 -90
- package/src/components/callQueue/CallQueueSettings.jsx +308 -146
- package/src/components/callQueue/callPopStatus.js +236 -0
- package/src/components/callQueue/callQueueHelpers.js +284 -2
- package/src/components/styles/CallQueueDiagnostics.module.scss +398 -0
- package/src/components/styles/CallQueuePop.module.scss +29 -0
- package/src/components/styles/CallQueueSettings.module.scss +93 -0
- package/src/index.js +10 -0
|
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
|
|
|
4
4
|
import Toggle from 'react-toggle';
|
|
5
5
|
import {
|
|
6
6
|
AlertTriangle,
|
|
7
|
+
ChevronDown,
|
|
7
8
|
ChevronRight,
|
|
8
9
|
Info,
|
|
9
10
|
Loader2,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from 'lucide-react';
|
|
13
14
|
|
|
14
15
|
import CustomFetch from '../Fetch';
|
|
16
|
+
import CallQueueDiagnostics from './CallQueueDiagnostics';
|
|
15
17
|
import styles from '../styles/CallQueueSettings.module.scss';
|
|
16
18
|
|
|
17
19
|
// Pure helpers live in a plain .js sibling so `node --test` can import them;
|
|
@@ -81,6 +83,57 @@ const defaultBreadcrumb = (title) => (
|
|
|
81
83
|
</div>
|
|
82
84
|
);
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The pseudo row's copy. Direct calls are not a Zoom call queue at all — they
|
|
88
|
+
* are the calls that ring one staff member's own extension — so the server
|
|
89
|
+
* appends a single made-up row for them, keyed 'direct', carrying the same two
|
|
90
|
+
* settings every queue row has.
|
|
91
|
+
*/
|
|
92
|
+
const DIRECT_SECTION_TITLE = 'Direct calls';
|
|
93
|
+
|
|
94
|
+
const DIRECT_INFO_NOTE =
|
|
95
|
+
'Intercepting a direct call in Zoom uses a call pickup GROUP code, ' +
|
|
96
|
+
'configured in Zoom admin — not a per-queue code. Enter that group’s code ' +
|
|
97
|
+
'here; leave it blank to pop direct calls without a Pick up button.';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Rows come back keyed `queue_id`, except the pseudo row, whose id the server
|
|
101
|
+
* sends as `id`. Filling it in here means every other line of this page — the
|
|
102
|
+
* draft map, dirty checks, saving — goes on treating rows identically.
|
|
103
|
+
*/
|
|
104
|
+
const normaliseRows = (rows) =>
|
|
105
|
+
(Array.isArray(rows) ? rows : []).map((row) =>
|
|
106
|
+
row?.queue_id === undefined || row?.queue_id === null
|
|
107
|
+
? { ...row, queue_id: row?.id }
|
|
108
|
+
: row
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Where the diagnostics disclosure remembers whether it was left open. A
|
|
113
|
+
* support session opens it once and every later page load starts there.
|
|
114
|
+
*/
|
|
115
|
+
const DIAGNOSTICS_STORAGE_KEY = 'callQueueDiagnosticsOpen';
|
|
116
|
+
|
|
117
|
+
/** Storage is optional everywhere: private mode, SSR, a locked-down browser. */
|
|
118
|
+
const readDiagnosticsOpen = () => {
|
|
119
|
+
try {
|
|
120
|
+
return window?.localStorage?.getItem(DIAGNOSTICS_STORAGE_KEY) === '1';
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const writeDiagnosticsOpen = (open) => {
|
|
127
|
+
try {
|
|
128
|
+
window?.localStorage?.setItem(
|
|
129
|
+
DIAGNOSTICS_STORAGE_KEY,
|
|
130
|
+
open ? '1' : '0'
|
|
131
|
+
);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// Nothing to do: the disclosure still works, it just forgets.
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
84
137
|
const CallQueueSettings = ({
|
|
85
138
|
endpoints,
|
|
86
139
|
title = 'Call Queues',
|
|
@@ -89,6 +142,7 @@ const CallQueueSettings = ({
|
|
|
89
142
|
infoNote = DEFAULT_INFO_NOTE,
|
|
90
143
|
codePrefix = '*99',
|
|
91
144
|
onSaved = null,
|
|
145
|
+
diagnostics = false,
|
|
92
146
|
}) => {
|
|
93
147
|
const [rows, setRows] = useState([]);
|
|
94
148
|
const [draft, setDraft] = useState({});
|
|
@@ -96,6 +150,8 @@ const CallQueueSettings = ({
|
|
|
96
150
|
const [isLoading, setIsLoading] = useState(true);
|
|
97
151
|
const [zoomUnreachable, setZoomUnreachable] = useState(false);
|
|
98
152
|
const [zoomError, setZoomError] = useState('');
|
|
153
|
+
// Collapsed by default — this is a support tool, not part of the daily job.
|
|
154
|
+
const [diagnosticsOpen, setDiagnosticsOpen] = useState(readDiagnosticsOpen);
|
|
99
155
|
|
|
100
156
|
// Plain string, so it can sit in the loader's dep list without an object
|
|
101
157
|
// identity re-running the request on every render.
|
|
@@ -140,7 +196,7 @@ const CallQueueSettings = ({
|
|
|
140
196
|
try {
|
|
141
197
|
const res = await CustomFetch(listUrl, 'GET', {});
|
|
142
198
|
const data = res?.data ?? {};
|
|
143
|
-
const queues = data.queues ?? [];
|
|
199
|
+
const queues = normaliseRows(data.queues ?? []);
|
|
144
200
|
|
|
145
201
|
setRows(queues);
|
|
146
202
|
seedDraft(queues);
|
|
@@ -245,6 +301,173 @@ const CallQueueSettings = ({
|
|
|
245
301
|
}
|
|
246
302
|
};
|
|
247
303
|
|
|
304
|
+
/**
|
|
305
|
+
* One settings row. Shared by the real call queues and by the pseudo
|
|
306
|
+
* 'direct' row, which carries the same two settings and saves through
|
|
307
|
+
* the same endpoint — only its identity column differs.
|
|
308
|
+
*/
|
|
309
|
+
const renderRow = (queue) => {
|
|
310
|
+
const current = draft[queue.queue_id] ?? {
|
|
311
|
+
pickup_code: '',
|
|
312
|
+
excluded: false,
|
|
313
|
+
};
|
|
314
|
+
const error = validateCode(current.pickup_code);
|
|
315
|
+
const dirty = isDirty(queue);
|
|
316
|
+
const isSaving = Boolean(saving[queue.queue_id]);
|
|
317
|
+
const isExcluded = Boolean(current.excluded);
|
|
318
|
+
const hasCode = Boolean(current.pickup_code);
|
|
319
|
+
const number = formatAuNumber(queue.number);
|
|
320
|
+
const name = queue.name || queue.queue_id;
|
|
321
|
+
// The 'direct' row is not a queue in Zoom: it has no number, no
|
|
322
|
+
// extension and no status to report, only the two settings.
|
|
323
|
+
const isPseudo = Boolean(queue.pseudo);
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<tr
|
|
327
|
+
key={queue.queue_id}
|
|
328
|
+
className={isExcluded ? styles.isExcluded : undefined}
|
|
329
|
+
>
|
|
330
|
+
<td>
|
|
331
|
+
<span className={styles.cqsettingsName}>
|
|
332
|
+
<span className={styles.cqsettingsIcon}>
|
|
333
|
+
<Phone size={13} />
|
|
334
|
+
</span>
|
|
335
|
+
<span title={name}>{name}</span>
|
|
336
|
+
</span>
|
|
337
|
+
{isPseudo ? null : (
|
|
338
|
+
<span className={styles.cqsettingsMeta}>
|
|
339
|
+
{queue.extension_number
|
|
340
|
+
? `Ext ${queue.extension_number}`
|
|
341
|
+
: 'No extension'}
|
|
342
|
+
{queue.missing_in_zoom && (
|
|
343
|
+
<span className={styles.cqsettingsFlag}>
|
|
344
|
+
not in Zoom any more
|
|
345
|
+
</span>
|
|
346
|
+
)}
|
|
347
|
+
</span>
|
|
348
|
+
)}
|
|
349
|
+
</td>
|
|
350
|
+
<td>
|
|
351
|
+
{number ? (
|
|
352
|
+
<span className={styles.cqsettingsNumber}>
|
|
353
|
+
{number}
|
|
354
|
+
</span>
|
|
355
|
+
) : (
|
|
356
|
+
<span className={styles.cqsettingsNone}>—</span>
|
|
357
|
+
)}
|
|
358
|
+
</td>
|
|
359
|
+
<td>
|
|
360
|
+
{isPseudo ? (
|
|
361
|
+
<span className={styles.cqsettingsNone}>—</span>
|
|
362
|
+
) : (
|
|
363
|
+
<span
|
|
364
|
+
className={
|
|
365
|
+
styles.cqsettingsPill +
|
|
366
|
+
(queue.status === 'active'
|
|
367
|
+
? ' ' + styles.cqsettingsPillOk
|
|
368
|
+
: '')
|
|
369
|
+
}
|
|
370
|
+
>
|
|
371
|
+
{titleCase(queue.status)}
|
|
372
|
+
</span>
|
|
373
|
+
)}
|
|
374
|
+
</td>
|
|
375
|
+
<td>
|
|
376
|
+
<span
|
|
377
|
+
className={
|
|
378
|
+
styles.cqsettingsCode +
|
|
379
|
+
(error ? ' ' + styles.hasError : '')
|
|
380
|
+
}
|
|
381
|
+
>
|
|
382
|
+
<span className={styles.cqsettingsCodePrefix}>
|
|
383
|
+
{codePrefix}
|
|
384
|
+
</span>
|
|
385
|
+
<input
|
|
386
|
+
className={styles.cqsettingsCodeInput}
|
|
387
|
+
type="text"
|
|
388
|
+
inputMode="numeric"
|
|
389
|
+
maxLength={4}
|
|
390
|
+
placeholder="0000"
|
|
391
|
+
aria-label={`Pickup code for ${name}`}
|
|
392
|
+
aria-invalid={Boolean(error)}
|
|
393
|
+
value={current.pickup_code}
|
|
394
|
+
onChange={(e) =>
|
|
395
|
+
setField(
|
|
396
|
+
queue.queue_id,
|
|
397
|
+
'pickup_code',
|
|
398
|
+
cleanCode(e.target.value)
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
/>
|
|
402
|
+
</span>
|
|
403
|
+
{error && (
|
|
404
|
+
<span className={styles.cqsettingsError}>{error}</span>
|
|
405
|
+
)}
|
|
406
|
+
</td>
|
|
407
|
+
<td>
|
|
408
|
+
{isExcluded ? (
|
|
409
|
+
<span className={styles.cqsettingsPill}>Excluded</span>
|
|
410
|
+
) : hasCode ? (
|
|
411
|
+
<span
|
|
412
|
+
className={`${styles.cqsettingsPill} ${styles.cqsettingsPillOk}`}
|
|
413
|
+
>
|
|
414
|
+
Pick up enabled
|
|
415
|
+
</span>
|
|
416
|
+
) : (
|
|
417
|
+
<span className={styles.cqsettingsPill}>
|
|
418
|
+
No pick-up code
|
|
419
|
+
</span>
|
|
420
|
+
)}
|
|
421
|
+
</td>
|
|
422
|
+
<td className={styles.isCentred}>
|
|
423
|
+
<span className={styles.cqsettingsToggle}>
|
|
424
|
+
<Toggle
|
|
425
|
+
icons={false}
|
|
426
|
+
checked={isExcluded}
|
|
427
|
+
aria-label={`Exclude ${name} from the call pop`}
|
|
428
|
+
onChange={(e) =>
|
|
429
|
+
setField(
|
|
430
|
+
queue.queue_id,
|
|
431
|
+
'excluded',
|
|
432
|
+
e.target.checked
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
/>
|
|
436
|
+
</span>
|
|
437
|
+
</td>
|
|
438
|
+
<td>
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
className={styles.cqsettingsSave}
|
|
442
|
+
disabled={!dirty || isSaving || Boolean(error)}
|
|
443
|
+
onClick={() => save(queue)}
|
|
444
|
+
>
|
|
445
|
+
{isSaving ? (
|
|
446
|
+
<Loader2 size={13} className={styles.spin} />
|
|
447
|
+
) : (
|
|
448
|
+
<Save size={13} />
|
|
449
|
+
)}
|
|
450
|
+
<span>{isSaving ? 'Saving' : 'Save'}</span>
|
|
451
|
+
</button>
|
|
452
|
+
</td>
|
|
453
|
+
</tr>
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// The real Zoom queues, and the server's single pseudo row for direct
|
|
458
|
+
// calls. An older server sends no pseudo row at all, and the section simply
|
|
459
|
+
// does not appear.
|
|
460
|
+
const queueRows = rows.filter((row) => !row.pseudo);
|
|
461
|
+
const directRow = rows.find((row) => row.pseudo) ?? null;
|
|
462
|
+
|
|
463
|
+
const toggleDiagnostics = () => {
|
|
464
|
+
setDiagnosticsOpen((open) => {
|
|
465
|
+
writeDiagnosticsOpen(!open);
|
|
466
|
+
|
|
467
|
+
return !open;
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
|
|
248
471
|
// `undefined` keeps the default header, `null` removes it, anything else is
|
|
249
472
|
// rendered in its place.
|
|
250
473
|
const header =
|
|
@@ -339,165 +562,104 @@ const CallQueueSettings = ({
|
|
|
339
562
|
</tr>
|
|
340
563
|
</thead>
|
|
341
564
|
<tbody>
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const error = validateCode(current.pickup_code);
|
|
348
|
-
const dirty = isDirty(queue);
|
|
349
|
-
const isSaving = Boolean(saving[queue.queue_id]);
|
|
350
|
-
const isExcluded = Boolean(current.excluded);
|
|
351
|
-
const hasCode = Boolean(current.pickup_code);
|
|
352
|
-
const number = formatAuNumber(queue.number);
|
|
353
|
-
const name = queue.name || queue.queue_id;
|
|
354
|
-
|
|
355
|
-
return (
|
|
356
|
-
<tr
|
|
357
|
-
key={queue.queue_id}
|
|
565
|
+
{queueRows.map(renderRow)}
|
|
566
|
+
{queueRows.length === 0 ? (
|
|
567
|
+
<tr>
|
|
568
|
+
<td
|
|
569
|
+
colSpan={7}
|
|
358
570
|
className={
|
|
359
|
-
|
|
360
|
-
? styles.isExcluded
|
|
361
|
-
: undefined
|
|
571
|
+
styles.cqsettingsEmptyCell
|
|
362
572
|
}
|
|
363
573
|
>
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
</span>
|
|
391
|
-
)}
|
|
392
|
-
</td>
|
|
393
|
-
<td>
|
|
574
|
+
No call queues found in the
|
|
575
|
+
Zoom account.
|
|
576
|
+
</td>
|
|
577
|
+
</tr>
|
|
578
|
+
) : null}
|
|
579
|
+
|
|
580
|
+
{/* Not a Zoom call queue: the calls that
|
|
581
|
+
ring one staff member's own extension.
|
|
582
|
+
Set apart so nobody reads it as a queue
|
|
583
|
+
they have never heard of. */}
|
|
584
|
+
{directRow ? (
|
|
585
|
+
<tr
|
|
586
|
+
className={
|
|
587
|
+
styles.cqsettingsSection
|
|
588
|
+
}
|
|
589
|
+
>
|
|
590
|
+
<td colSpan={7}>
|
|
591
|
+
<span
|
|
592
|
+
className={
|
|
593
|
+
styles.cqsettingsSectionTitle
|
|
594
|
+
}
|
|
595
|
+
>
|
|
596
|
+
{directRow.name ||
|
|
597
|
+
DIRECT_SECTION_TITLE}
|
|
598
|
+
</span>
|
|
599
|
+
{directRow.description ? (
|
|
394
600
|
<span
|
|
395
601
|
className={
|
|
396
|
-
styles.
|
|
397
|
-
(queue.status === 'active'
|
|
398
|
-
? ' ' + styles.cqsettingsPillOk
|
|
399
|
-
: '')
|
|
602
|
+
styles.cqsettingsSectionText
|
|
400
603
|
}
|
|
401
604
|
>
|
|
402
|
-
{
|
|
403
|
-
|
|
404
|
-
</td>
|
|
405
|
-
<td>
|
|
406
|
-
<span
|
|
407
|
-
className={
|
|
408
|
-
styles.cqsettingsCode +
|
|
409
|
-
(error ? ' ' + styles.hasError : '')
|
|
605
|
+
{
|
|
606
|
+
directRow.description
|
|
410
607
|
}
|
|
411
|
-
>
|
|
412
|
-
<span className={styles.cqsettingsCodePrefix}>
|
|
413
|
-
{codePrefix}
|
|
414
|
-
</span>
|
|
415
|
-
<input
|
|
416
|
-
className={styles.cqsettingsCodeInput}
|
|
417
|
-
type="text"
|
|
418
|
-
inputMode="numeric"
|
|
419
|
-
maxLength={4}
|
|
420
|
-
placeholder="0000"
|
|
421
|
-
aria-label={`Pickup code for ${name}`}
|
|
422
|
-
aria-invalid={Boolean(error)}
|
|
423
|
-
value={current.pickup_code}
|
|
424
|
-
onChange={(e) =>
|
|
425
|
-
setField(
|
|
426
|
-
queue.queue_id,
|
|
427
|
-
'pickup_code',
|
|
428
|
-
cleanCode(e.target.value)
|
|
429
|
-
)
|
|
430
|
-
}
|
|
431
|
-
/>
|
|
432
608
|
</span>
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
Excluded
|
|
443
|
-
</span>
|
|
444
|
-
) : hasCode ? (
|
|
445
|
-
<span
|
|
446
|
-
className={`${styles.cqsettingsPill} ${styles.cqsettingsPillOk}`}
|
|
447
|
-
>
|
|
448
|
-
Pick up enabled
|
|
449
|
-
</span>
|
|
450
|
-
) : (
|
|
451
|
-
<span className={styles.cqsettingsPill}>
|
|
452
|
-
No pick-up code
|
|
453
|
-
</span>
|
|
454
|
-
)}
|
|
455
|
-
</td>
|
|
456
|
-
<td className={styles.isCentred}>
|
|
457
|
-
<span className={styles.cqsettingsToggle}>
|
|
458
|
-
<Toggle
|
|
459
|
-
icons={false}
|
|
460
|
-
checked={isExcluded}
|
|
461
|
-
aria-label={`Exclude ${name} from the call pop`}
|
|
462
|
-
onChange={(e) =>
|
|
463
|
-
setField(
|
|
464
|
-
queue.queue_id,
|
|
465
|
-
'excluded',
|
|
466
|
-
e.target.checked
|
|
467
|
-
)
|
|
468
|
-
}
|
|
469
|
-
/>
|
|
609
|
+
) : null}
|
|
610
|
+
<span
|
|
611
|
+
className={
|
|
612
|
+
styles.cqsettingsSectionNote
|
|
613
|
+
}
|
|
614
|
+
>
|
|
615
|
+
<Info size={14} />
|
|
616
|
+
<span>
|
|
617
|
+
{DIRECT_INFO_NOTE}
|
|
470
618
|
</span>
|
|
471
|
-
</
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
onClick={() => save(queue)}
|
|
480
|
-
>
|
|
481
|
-
{isSaving ? (
|
|
482
|
-
<Loader2
|
|
483
|
-
size={13}
|
|
484
|
-
className={styles.spin}
|
|
485
|
-
/>
|
|
486
|
-
) : (
|
|
487
|
-
<Save size={13} />
|
|
488
|
-
)}
|
|
489
|
-
<span>
|
|
490
|
-
{isSaving ? 'Saving' : 'Save'}
|
|
491
|
-
</span>
|
|
492
|
-
</button>
|
|
493
|
-
</td>
|
|
494
|
-
</tr>
|
|
495
|
-
);
|
|
496
|
-
})}
|
|
619
|
+
</span>
|
|
620
|
+
</td>
|
|
621
|
+
</tr>
|
|
622
|
+
) : null}
|
|
623
|
+
{directRow
|
|
624
|
+
? renderRow(directRow)
|
|
625
|
+
: null}
|
|
497
626
|
</tbody>
|
|
498
627
|
</table>
|
|
499
628
|
)}
|
|
500
629
|
</div>
|
|
630
|
+
|
|
631
|
+
{/* Support tooling, folded away: why a pop did or did
|
|
632
|
+
not appear, from both ends of the pipeline. */}
|
|
633
|
+
{diagnostics ? (
|
|
634
|
+
<div className={styles.cqsettingsDiagnostics}>
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
className={styles.cqsettingsDisclosure}
|
|
638
|
+
aria-expanded={diagnosticsOpen}
|
|
639
|
+
aria-controls="cqsettings-diagnostics"
|
|
640
|
+
onClick={toggleDiagnostics}
|
|
641
|
+
>
|
|
642
|
+
{diagnosticsOpen ? (
|
|
643
|
+
<ChevronDown size={15} />
|
|
644
|
+
) : (
|
|
645
|
+
<ChevronRight size={15} />
|
|
646
|
+
)}
|
|
647
|
+
<span>Diagnostics</span>
|
|
648
|
+
</button>
|
|
649
|
+
|
|
650
|
+
{diagnosticsOpen ? (
|
|
651
|
+
<div id="cqsettings-diagnostics">
|
|
652
|
+
<CallQueueDiagnostics
|
|
653
|
+
endpoints={
|
|
654
|
+
diagnostics?.endpoints ??
|
|
655
|
+
undefined
|
|
656
|
+
}
|
|
657
|
+
embedded
|
|
658
|
+
/>
|
|
659
|
+
</div>
|
|
660
|
+
) : null}
|
|
661
|
+
</div>
|
|
662
|
+
) : null}
|
|
501
663
|
</div>
|
|
502
664
|
</div>
|
|
503
665
|
</div>
|