@visns-studio/visns-components 6.6.4 → 6.15.1
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/README.md +1623 -18
- package/package.json +10 -4
- package/src/components/DataGrid.jsx +181 -38
- package/src/components/Fetch.jsx +80 -3
- package/src/components/Form.jsx +9 -0
- package/src/components/Navigation.jsx +109 -50
- package/src/components/Notification.jsx +279 -9
- package/src/components/TableFilter.jsx +14 -1
- package/src/components/auth/AuthBrandPanel.jsx +43 -0
- package/src/components/auth/AuthLoading.jsx +78 -0
- package/src/components/auth/AuthShell.jsx +59 -0
- package/src/components/auth/ClientAuth.jsx +161 -0
- package/src/components/auth/ClientAuthFrame.jsx +59 -0
- package/src/components/auth/ClientLogin.jsx +266 -55
- package/src/components/auth/ClientOTPVerify.jsx +587 -115
- package/src/components/auth/ImpersonateGate.jsx +254 -0
- package/src/components/auth/Login.jsx +134 -41
- package/src/components/auth/LogoutScreen.jsx +112 -0
- package/src/components/auth/Reset.jsx +134 -77
- package/src/components/auth/TwoFactorAuth.jsx +475 -297
- package/src/components/auth/Verify.jsx +237 -126
- package/src/components/auth/authEndpoints.js +105 -0
- package/src/components/auth/authFont.js +23 -0
- package/src/components/auth/authHelpers.js +465 -0
- package/src/components/auth/clientAuthProtocols.js +240 -0
- package/src/components/auth/useOptionalRouter.js +49 -0
- package/src/components/callQueue/CallQueuePop.jsx +1502 -0
- package/src/components/callQueue/CallQueueSettings.jsx +508 -0
- package/src/components/callQueue/callQueueHelpers.js +280 -0
- package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
- package/src/components/columns/ColumnRenderers.jsx +53 -7
- package/src/components/generic/GenericAuth.jsx +163 -96
- package/src/components/generic/GenericDetail.jsx +215 -90
- package/src/components/generic/GenericIndex.jsx +20 -47
- package/src/components/generic/GenericMain.jsx +5 -0
- package/src/components/generic/GroupedReportRenderer.jsx +1 -5
- package/src/components/generic/StandardModal.jsx +17 -5
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
- package/src/components/navActive.js +60 -0
- package/src/components/notify/desktopNotifications.js +256 -0
- package/src/components/sketch/SketchField.jsx +12 -2
- package/src/components/sms/SmsComposeModal.jsx +495 -0
- package/src/components/sms/SmsInbox.jsx +596 -0
- package/src/components/sms/SmsInboxBadge.jsx +468 -0
- package/src/components/sms/SmsLineSettings.jsx +854 -0
- package/src/components/sms/SmsThreadPanel.jsx +1184 -0
- package/src/components/sms/smsEndpoints.js +56 -0
- package/src/components/sms/smsHelpers.js +1075 -0
- package/src/components/sms/smsLiveState.js +132 -0
- package/src/components/sms/useSmsLive.js +307 -0
- package/src/components/styles/CallQueuePop.module.scss +646 -0
- package/src/components/styles/CallQueueSettings.module.scss +460 -0
- package/src/components/styles/ClientAuth.module.scss +229 -0
- package/src/components/styles/GenericDetail.module.scss +153 -26
- package/src/components/styles/GenericIndex.module.scss +13 -0
- package/src/components/styles/GenericMain.module.scss +19 -2
- package/src/components/styles/ImpersonateGate.module.scss +85 -0
- package/src/components/styles/Login.module.scss +13 -122
- package/src/components/styles/LogoutScreen.module.scss +89 -0
- package/src/components/styles/Navigation.module.scss +509 -223
- package/src/components/styles/Notification.module.scss +103 -0
- package/src/components/styles/Reset.module.scss +52 -186
- package/src/components/styles/Sms.module.scss +2382 -0
- package/src/components/styles/TableFilter.module.scss +118 -98
- package/src/components/styles/TwoFactorAuth.module.scss +115 -176
- package/src/components/styles/Vault.module.scss +1983 -0
- package/src/components/styles/Verify.module.scss +57 -180
- package/src/components/styles/_authBrandPanel.scss +163 -0
- package/src/components/styles/_authShell.scss +369 -0
- package/src/components/styles/global-datagrid.css +27 -2
- package/src/components/utils/buildEnv.js +67 -0
- package/src/components/utils/displayValue.js +94 -0
- package/src/components/utils/useDensity.js +345 -9
- package/src/components/vault/OtpChip.jsx +208 -0
- package/src/components/vault/PasswordGenerator.jsx +145 -0
- package/src/components/vault/QrScanner.jsx +326 -0
- package/src/components/vault/VaultAccessLog.jsx +225 -0
- package/src/components/vault/VaultConfirmPanel.jsx +113 -0
- package/src/components/vault/VaultEntryForm.jsx +740 -0
- package/src/components/vault/VaultManager.jsx +1000 -0
- package/src/components/vault/VaultQuickSearch.jsx +681 -0
- package/src/components/vault/qrDecode.js +257 -0
- package/src/components/vault/useDebouncedValue.js +22 -0
- package/src/components/vault/useVaultReveal.js +160 -0
- package/src/components/vault/vaultClipboard.js +33 -0
- package/src/components/vault/vaultEndpoints.js +40 -0
- package/src/components/vault/vaultFit.js +116 -0
- package/src/components/vault/vaultHelpers.js +547 -0
- package/src/components/vault/vaultNavigation.jsx +58 -0
- package/src/components/vault/vaultOtp.js +105 -0
- package/src/index.js +233 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { toast } from 'react-toastify';
|
|
4
|
+
import Toggle from 'react-toggle';
|
|
5
|
+
import {
|
|
6
|
+
AlertTriangle,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
Info,
|
|
9
|
+
Loader2,
|
|
10
|
+
Phone,
|
|
11
|
+
Save,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
import CustomFetch from '../Fetch';
|
|
15
|
+
import styles from '../styles/CallQueueSettings.module.scss';
|
|
16
|
+
|
|
17
|
+
// Pure helpers live in a plain .js sibling so `node --test` can import them;
|
|
18
|
+
// re-exported below so this file's public surface is unchanged.
|
|
19
|
+
import {
|
|
20
|
+
cleanCode,
|
|
21
|
+
formatAuNumber,
|
|
22
|
+
titleCase,
|
|
23
|
+
validateCode,
|
|
24
|
+
} from './callQueueSettingsHelpers';
|
|
25
|
+
|
|
26
|
+
export { cleanCode, formatAuNumber, titleCase, validateCode };
|
|
27
|
+
|
|
28
|
+
import 'react-toggle/style.css';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Settings -> Call Queues.
|
|
32
|
+
*
|
|
33
|
+
* One row per Zoom call queue (the list is live from the Zoom API), carrying the
|
|
34
|
+
* two things the app owns: the pickup code the call pop tells staff to dial, and
|
|
35
|
+
* whether the queue is allowed to pop at all.
|
|
36
|
+
*
|
|
37
|
+
* Saving a code pushes it to Zoom before it is stored, so a rejected code never
|
|
38
|
+
* ends up on a Pick up button — the row surfaces Zoom's own wording when that
|
|
39
|
+
* happens. Excluding a queue is local only and keeps working while Zoom is
|
|
40
|
+
* unreachable, which is why an API failure warns rather than empties the page.
|
|
41
|
+
*
|
|
42
|
+
* Library note: the endpoints, the page header and every line of copy are props
|
|
43
|
+
* whose defaults reproduce the CRM's original page exactly.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** Endpoints the page talks to; the `endpoints` prop is merged over these. */
|
|
47
|
+
const DEFAULT_ENDPOINTS = {
|
|
48
|
+
list: '/ajax/call-queue/settings',
|
|
49
|
+
save: (queueId) => '/ajax/call-queue/settings/' + encodeURIComponent(queueId),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The page's default copy, kept verbatim from the CRM. */
|
|
53
|
+
const DEFAULT_INTRO =
|
|
54
|
+
'Every Zoom call queue pops a card for staff with the Call Queue Monitor ' +
|
|
55
|
+
'permission — set the code that card tells them to dial, or keep a queue ' +
|
|
56
|
+
'out of the pop entirely.';
|
|
57
|
+
|
|
58
|
+
const DEFAULT_INFO_NOTE =
|
|
59
|
+
"Saving a code switches the pickup code policy on for that queue in Zoom " +
|
|
60
|
+
"(clearing it switches the policy off), but Zoom's API will not accept the " +
|
|
61
|
+
'digits themselves — a new code still has to be typed into the Zoom admin ' +
|
|
62
|
+
'UI. What is stored here is what the call pop tells staff to dial.';
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The page header the CRM shipped: `Settings > Call Queues` in the app's own
|
|
67
|
+
* `crmtitle` block. Rendered unless the `breadcrumb` prop replaces it (any React
|
|
68
|
+
* node) or switches it off entirely (`null`).
|
|
69
|
+
*/
|
|
70
|
+
const defaultBreadcrumb = (title) => (
|
|
71
|
+
<div className="grid">
|
|
72
|
+
<div className="grid__row">
|
|
73
|
+
<div className="grid__full crmtitle">
|
|
74
|
+
<h1>
|
|
75
|
+
<Link to="/settings">Settings</Link>
|
|
76
|
+
<ChevronRight strokeWidth={2} size={18} />
|
|
77
|
+
{title}
|
|
78
|
+
</h1>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const CallQueueSettings = ({
|
|
85
|
+
endpoints,
|
|
86
|
+
title = 'Call Queues',
|
|
87
|
+
breadcrumb,
|
|
88
|
+
intro = DEFAULT_INTRO,
|
|
89
|
+
infoNote = DEFAULT_INFO_NOTE,
|
|
90
|
+
codePrefix = '*99',
|
|
91
|
+
onSaved = null,
|
|
92
|
+
}) => {
|
|
93
|
+
const [rows, setRows] = useState([]);
|
|
94
|
+
const [draft, setDraft] = useState({});
|
|
95
|
+
const [saving, setSaving] = useState({});
|
|
96
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
97
|
+
const [zoomUnreachable, setZoomUnreachable] = useState(false);
|
|
98
|
+
const [zoomError, setZoomError] = useState('');
|
|
99
|
+
|
|
100
|
+
// Plain string, so it can sit in the loader's dep list without an object
|
|
101
|
+
// identity re-running the request on every render.
|
|
102
|
+
const listUrl = endpoints?.list ?? DEFAULT_ENDPOINTS.list;
|
|
103
|
+
|
|
104
|
+
/** The save endpoint for one queue, from a builder or an `{id}` template. */
|
|
105
|
+
const saveUrl = useCallback(
|
|
106
|
+
(queueId) => {
|
|
107
|
+
const option = endpoints?.save;
|
|
108
|
+
|
|
109
|
+
if (typeof option === 'function') {
|
|
110
|
+
return option(queueId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof option === 'string' && option !== '') {
|
|
114
|
+
return option
|
|
115
|
+
.split('{id}')
|
|
116
|
+
.join(encodeURIComponent(String(queueId)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return DEFAULT_ENDPOINTS.save(queueId);
|
|
120
|
+
},
|
|
121
|
+
[endpoints]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const seedDraft = useCallback((queues) => {
|
|
125
|
+
const next = {};
|
|
126
|
+
|
|
127
|
+
queues.forEach((queue) => {
|
|
128
|
+
next[queue.queue_id] = {
|
|
129
|
+
pickup_code: queue.pickup_code ?? '',
|
|
130
|
+
excluded: Boolean(queue.excluded),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
setDraft(next);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const load = useCallback(async () => {
|
|
138
|
+
setIsLoading(true);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const res = await CustomFetch(listUrl, 'GET', {});
|
|
142
|
+
const data = res?.data ?? {};
|
|
143
|
+
const queues = data.queues ?? [];
|
|
144
|
+
|
|
145
|
+
setRows(queues);
|
|
146
|
+
seedDraft(queues);
|
|
147
|
+
setZoomUnreachable(Boolean(data.zoom_unreachable));
|
|
148
|
+
setZoomError(String(data.error ?? ''));
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error('Failed to load call queue settings:', e);
|
|
151
|
+
} finally {
|
|
152
|
+
setIsLoading(false);
|
|
153
|
+
}
|
|
154
|
+
}, [listUrl, seedDraft]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
load();
|
|
158
|
+
}, [load]);
|
|
159
|
+
|
|
160
|
+
const setField = (queueId, field, value) => {
|
|
161
|
+
setDraft((prev) => ({
|
|
162
|
+
...prev,
|
|
163
|
+
[queueId]: { ...prev[queueId], [field]: value },
|
|
164
|
+
}));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Nothing to save until the row differs from what the server returned. */
|
|
168
|
+
const isDirty = (queue) => {
|
|
169
|
+
const current = draft[queue.queue_id];
|
|
170
|
+
|
|
171
|
+
if (!current) return false;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
current.pickup_code !== (queue.pickup_code ?? '') ||
|
|
175
|
+
Boolean(current.excluded) !== Boolean(queue.excluded)
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const save = async (queue) => {
|
|
180
|
+
const queueId = queue.queue_id;
|
|
181
|
+
const current = draft[queueId] ?? {};
|
|
182
|
+
const error = validateCode(current.pickup_code);
|
|
183
|
+
|
|
184
|
+
if (error) {
|
|
185
|
+
toast.error(error);
|
|
186
|
+
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setSaving((prev) => ({ ...prev, [queueId]: true }));
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const res = await CustomFetch(
|
|
194
|
+
saveUrl(queueId),
|
|
195
|
+
'PUT',
|
|
196
|
+
{
|
|
197
|
+
pickup_code:
|
|
198
|
+
current.pickup_code === '' ? null : current.pickup_code,
|
|
199
|
+
excluded: Boolean(current.excluded),
|
|
200
|
+
// Keeps the local name cache fresh even when this save is the
|
|
201
|
+
// only thing that touches the row today.
|
|
202
|
+
queue_name: queue.name ?? null,
|
|
203
|
+
},
|
|
204
|
+
null,
|
|
205
|
+
// Own the error toast: CustomFetch would otherwise raise its own
|
|
206
|
+
// and the row would have no idea the save failed.
|
|
207
|
+
(message) => {
|
|
208
|
+
toast.error(message || 'Failed to save the queue settings.');
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const saved = res?.data?.queue;
|
|
213
|
+
|
|
214
|
+
if (saved) {
|
|
215
|
+
setRows((prev) =>
|
|
216
|
+
prev.map((row) =>
|
|
217
|
+
row.queue_id === queueId
|
|
218
|
+
? {
|
|
219
|
+
...row,
|
|
220
|
+
pickup_code: saved.pickup_code,
|
|
221
|
+
excluded: saved.excluded,
|
|
222
|
+
}
|
|
223
|
+
: row
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
setDraft((prev) => ({
|
|
227
|
+
...prev,
|
|
228
|
+
[queueId]: {
|
|
229
|
+
pickup_code: saved.pickup_code ?? '',
|
|
230
|
+
excluded: Boolean(saved.excluded),
|
|
231
|
+
},
|
|
232
|
+
}));
|
|
233
|
+
toast.success('Call queue settings saved.');
|
|
234
|
+
|
|
235
|
+
if (typeof onSaved === 'function') {
|
|
236
|
+
onSaved(saved);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// The error toast is already out (errorCallback above); this only
|
|
241
|
+
// stops an unhandled rejection.
|
|
242
|
+
console.error('Failed to save call queue settings:', e);
|
|
243
|
+
} finally {
|
|
244
|
+
setSaving((prev) => ({ ...prev, [queueId]: false }));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// `undefined` keeps the default header, `null` removes it, anything else is
|
|
249
|
+
// rendered in its place.
|
|
250
|
+
const header =
|
|
251
|
+
breadcrumb === undefined ? defaultBreadcrumb(title) : breadcrumb;
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div>
|
|
255
|
+
{header}
|
|
256
|
+
|
|
257
|
+
<div className="grid">
|
|
258
|
+
<div className="grid__full">
|
|
259
|
+
<div className={styles.cqsettings}>
|
|
260
|
+
{intro ? (
|
|
261
|
+
<p className={styles.cqsettingsIntro}>{intro}</p>
|
|
262
|
+
) : null}
|
|
263
|
+
|
|
264
|
+
{infoNote ? (
|
|
265
|
+
<div className={styles.cqsettingsInfo}>
|
|
266
|
+
<Info size={15} />
|
|
267
|
+
<span>{infoNote}</span>
|
|
268
|
+
</div>
|
|
269
|
+
) : null}
|
|
270
|
+
|
|
271
|
+
{zoomUnreachable && (
|
|
272
|
+
<div className={styles.cqsettingsWarning}>
|
|
273
|
+
<AlertTriangle size={15} />
|
|
274
|
+
<span>
|
|
275
|
+
Zoom could not be reached
|
|
276
|
+
{zoomError ? ` (${zoomError})` : ''}, so only
|
|
277
|
+
queues already configured here are listed and
|
|
278
|
+
pickup codes cannot be saved. Exclusions still
|
|
279
|
+
work.
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
<div className={styles.cqsettingsPanel}>
|
|
285
|
+
{isLoading ? (
|
|
286
|
+
<div className={styles.cqsettingsState}>
|
|
287
|
+
<Loader2 size={16} className={styles.spin} />
|
|
288
|
+
<span>Loading call queues...</span>
|
|
289
|
+
</div>
|
|
290
|
+
) : rows.length === 0 ? (
|
|
291
|
+
<div className={styles.cqsettingsState}>
|
|
292
|
+
<span>
|
|
293
|
+
No call queues found in the Zoom account.
|
|
294
|
+
</span>
|
|
295
|
+
</div>
|
|
296
|
+
) : (
|
|
297
|
+
<table className={styles.cqsettingsTable}>
|
|
298
|
+
{/* Fixed layout + a colgroup: the queue name
|
|
299
|
+
takes the slack, every other column is
|
|
300
|
+
sized to the control it holds. */}
|
|
301
|
+
<colgroup>
|
|
302
|
+
<col />
|
|
303
|
+
<col
|
|
304
|
+
className={
|
|
305
|
+
styles.cqsettingsColNumber
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
<col
|
|
309
|
+
className={
|
|
310
|
+
styles.cqsettingsColStatus
|
|
311
|
+
}
|
|
312
|
+
/>
|
|
313
|
+
<col
|
|
314
|
+
className={styles.cqsettingsColCode}
|
|
315
|
+
/>
|
|
316
|
+
<col
|
|
317
|
+
className={styles.cqsettingsColPop}
|
|
318
|
+
/>
|
|
319
|
+
<col
|
|
320
|
+
className={
|
|
321
|
+
styles.cqsettingsColExclude
|
|
322
|
+
}
|
|
323
|
+
/>
|
|
324
|
+
<col
|
|
325
|
+
className={styles.cqsettingsColSave}
|
|
326
|
+
/>
|
|
327
|
+
</colgroup>
|
|
328
|
+
<thead>
|
|
329
|
+
<tr>
|
|
330
|
+
<th>Queue</th>
|
|
331
|
+
<th>Number</th>
|
|
332
|
+
<th>Status</th>
|
|
333
|
+
<th>Pickup code</th>
|
|
334
|
+
<th>Call pop</th>
|
|
335
|
+
<th className={styles.isCentred}>
|
|
336
|
+
Exclude
|
|
337
|
+
</th>
|
|
338
|
+
<th aria-label="Save" />
|
|
339
|
+
</tr>
|
|
340
|
+
</thead>
|
|
341
|
+
<tbody>
|
|
342
|
+
{rows.map((queue) => {
|
|
343
|
+
const current = draft[queue.queue_id] ?? {
|
|
344
|
+
pickup_code: '',
|
|
345
|
+
excluded: false,
|
|
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}
|
|
358
|
+
className={
|
|
359
|
+
isExcluded
|
|
360
|
+
? styles.isExcluded
|
|
361
|
+
: undefined
|
|
362
|
+
}
|
|
363
|
+
>
|
|
364
|
+
<td>
|
|
365
|
+
<span className={styles.cqsettingsName}>
|
|
366
|
+
<span className={styles.cqsettingsIcon}>
|
|
367
|
+
<Phone size={13} />
|
|
368
|
+
</span>
|
|
369
|
+
<span title={name}>{name}</span>
|
|
370
|
+
</span>
|
|
371
|
+
<span className={styles.cqsettingsMeta}>
|
|
372
|
+
{queue.extension_number
|
|
373
|
+
? `Ext ${queue.extension_number}`
|
|
374
|
+
: 'No extension'}
|
|
375
|
+
{queue.missing_in_zoom && (
|
|
376
|
+
<span className={styles.cqsettingsFlag}>
|
|
377
|
+
not in Zoom any more
|
|
378
|
+
</span>
|
|
379
|
+
)}
|
|
380
|
+
</span>
|
|
381
|
+
</td>
|
|
382
|
+
<td>
|
|
383
|
+
{number ? (
|
|
384
|
+
<span className={styles.cqsettingsNumber}>
|
|
385
|
+
{number}
|
|
386
|
+
</span>
|
|
387
|
+
) : (
|
|
388
|
+
<span className={styles.cqsettingsNone}>
|
|
389
|
+
—
|
|
390
|
+
</span>
|
|
391
|
+
)}
|
|
392
|
+
</td>
|
|
393
|
+
<td>
|
|
394
|
+
<span
|
|
395
|
+
className={
|
|
396
|
+
styles.cqsettingsPill +
|
|
397
|
+
(queue.status === 'active'
|
|
398
|
+
? ' ' + styles.cqsettingsPillOk
|
|
399
|
+
: '')
|
|
400
|
+
}
|
|
401
|
+
>
|
|
402
|
+
{titleCase(queue.status)}
|
|
403
|
+
</span>
|
|
404
|
+
</td>
|
|
405
|
+
<td>
|
|
406
|
+
<span
|
|
407
|
+
className={
|
|
408
|
+
styles.cqsettingsCode +
|
|
409
|
+
(error ? ' ' + styles.hasError : '')
|
|
410
|
+
}
|
|
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
|
+
</span>
|
|
433
|
+
{error && (
|
|
434
|
+
<span className={styles.cqsettingsError}>
|
|
435
|
+
{error}
|
|
436
|
+
</span>
|
|
437
|
+
)}
|
|
438
|
+
</td>
|
|
439
|
+
<td>
|
|
440
|
+
{isExcluded ? (
|
|
441
|
+
<span className={styles.cqsettingsPill}>
|
|
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
|
+
/>
|
|
470
|
+
</span>
|
|
471
|
+
</td>
|
|
472
|
+
<td>
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
className={styles.cqsettingsSave}
|
|
476
|
+
disabled={
|
|
477
|
+
!dirty || isSaving || Boolean(error)
|
|
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
|
+
})}
|
|
497
|
+
</tbody>
|
|
498
|
+
</table>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
export default CallQueueSettings;
|