@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.
Files changed (98) hide show
  1. package/README.md +1623 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +181 -38
  4. package/src/components/Fetch.jsx +80 -3
  5. package/src/components/Form.jsx +9 -0
  6. package/src/components/Navigation.jsx +109 -50
  7. package/src/components/Notification.jsx +279 -9
  8. package/src/components/TableFilter.jsx +14 -1
  9. package/src/components/auth/AuthBrandPanel.jsx +43 -0
  10. package/src/components/auth/AuthLoading.jsx +78 -0
  11. package/src/components/auth/AuthShell.jsx +59 -0
  12. package/src/components/auth/ClientAuth.jsx +161 -0
  13. package/src/components/auth/ClientAuthFrame.jsx +59 -0
  14. package/src/components/auth/ClientLogin.jsx +266 -55
  15. package/src/components/auth/ClientOTPVerify.jsx +587 -115
  16. package/src/components/auth/ImpersonateGate.jsx +254 -0
  17. package/src/components/auth/Login.jsx +134 -41
  18. package/src/components/auth/LogoutScreen.jsx +112 -0
  19. package/src/components/auth/Reset.jsx +134 -77
  20. package/src/components/auth/TwoFactorAuth.jsx +475 -297
  21. package/src/components/auth/Verify.jsx +237 -126
  22. package/src/components/auth/authEndpoints.js +105 -0
  23. package/src/components/auth/authFont.js +23 -0
  24. package/src/components/auth/authHelpers.js +465 -0
  25. package/src/components/auth/clientAuthProtocols.js +240 -0
  26. package/src/components/auth/useOptionalRouter.js +49 -0
  27. package/src/components/callQueue/CallQueuePop.jsx +1502 -0
  28. package/src/components/callQueue/CallQueueSettings.jsx +508 -0
  29. package/src/components/callQueue/callQueueHelpers.js +280 -0
  30. package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
  31. package/src/components/columns/ColumnRenderers.jsx +53 -7
  32. package/src/components/generic/GenericAuth.jsx +163 -96
  33. package/src/components/generic/GenericDetail.jsx +215 -90
  34. package/src/components/generic/GenericIndex.jsx +20 -47
  35. package/src/components/generic/GenericMain.jsx +5 -0
  36. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  37. package/src/components/generic/StandardModal.jsx +17 -5
  38. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  39. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  40. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  41. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  42. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  44. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  45. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  46. package/src/components/navActive.js +60 -0
  47. package/src/components/notify/desktopNotifications.js +256 -0
  48. package/src/components/sketch/SketchField.jsx +12 -2
  49. package/src/components/sms/SmsComposeModal.jsx +495 -0
  50. package/src/components/sms/SmsInbox.jsx +596 -0
  51. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  52. package/src/components/sms/SmsLineSettings.jsx +854 -0
  53. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  54. package/src/components/sms/smsEndpoints.js +56 -0
  55. package/src/components/sms/smsHelpers.js +1075 -0
  56. package/src/components/sms/smsLiveState.js +132 -0
  57. package/src/components/sms/useSmsLive.js +307 -0
  58. package/src/components/styles/CallQueuePop.module.scss +646 -0
  59. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  60. package/src/components/styles/ClientAuth.module.scss +229 -0
  61. package/src/components/styles/GenericDetail.module.scss +153 -26
  62. package/src/components/styles/GenericIndex.module.scss +13 -0
  63. package/src/components/styles/GenericMain.module.scss +19 -2
  64. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  65. package/src/components/styles/Login.module.scss +13 -122
  66. package/src/components/styles/LogoutScreen.module.scss +89 -0
  67. package/src/components/styles/Navigation.module.scss +509 -223
  68. package/src/components/styles/Notification.module.scss +103 -0
  69. package/src/components/styles/Reset.module.scss +52 -186
  70. package/src/components/styles/Sms.module.scss +2382 -0
  71. package/src/components/styles/TableFilter.module.scss +118 -98
  72. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  73. package/src/components/styles/Vault.module.scss +1983 -0
  74. package/src/components/styles/Verify.module.scss +57 -180
  75. package/src/components/styles/_authBrandPanel.scss +163 -0
  76. package/src/components/styles/_authShell.scss +369 -0
  77. package/src/components/styles/global-datagrid.css +27 -2
  78. package/src/components/utils/buildEnv.js +67 -0
  79. package/src/components/utils/displayValue.js +94 -0
  80. package/src/components/utils/useDensity.js +345 -9
  81. package/src/components/vault/OtpChip.jsx +208 -0
  82. package/src/components/vault/PasswordGenerator.jsx +145 -0
  83. package/src/components/vault/QrScanner.jsx +326 -0
  84. package/src/components/vault/VaultAccessLog.jsx +225 -0
  85. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  86. package/src/components/vault/VaultEntryForm.jsx +740 -0
  87. package/src/components/vault/VaultManager.jsx +1000 -0
  88. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  89. package/src/components/vault/qrDecode.js +257 -0
  90. package/src/components/vault/useDebouncedValue.js +22 -0
  91. package/src/components/vault/useVaultReveal.js +160 -0
  92. package/src/components/vault/vaultClipboard.js +33 -0
  93. package/src/components/vault/vaultEndpoints.js +40 -0
  94. package/src/components/vault/vaultFit.js +116 -0
  95. package/src/components/vault/vaultHelpers.js +547 -0
  96. package/src/components/vault/vaultNavigation.jsx +58 -0
  97. package/src/components/vault/vaultOtp.js +105 -0
  98. 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;