@visns-studio/visns-components 6.17.1 → 6.20.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.
Files changed (84) hide show
  1. package/package.json +10 -7
  2. package/src/components/AssociationManager.jsx +344 -177
  3. package/src/components/AsyncSelect.jsx +2 -2
  4. package/src/components/Autocomplete.jsx +57 -12
  5. package/src/components/Breadcrumb.jsx +23 -4
  6. package/src/components/ClientAssociationManager.jsx +321 -145
  7. package/src/components/DataGrid.jsx +625 -20
  8. package/src/components/DropZone.jsx +4 -1
  9. package/src/components/Form.jsx +233 -6
  10. package/src/components/MultiSelect.jsx +2 -2
  11. package/src/components/Navigation.jsx +45 -25
  12. package/src/components/TableFilter.jsx +172 -12
  13. package/src/components/UniversalSearch.jsx +390 -0
  14. package/src/components/auth/Login.jsx +164 -8
  15. package/src/components/auth/Profile.jsx +209 -22
  16. package/src/components/auth/authEndpoints.js +4 -0
  17. package/src/components/auth/webauthn.js +194 -0
  18. package/src/components/columns/ColumnRenderers.jsx +6 -1
  19. package/src/components/controls/DataGridSearch.jsx +11 -5
  20. package/src/components/generic/GenericAuth.jsx +6 -0
  21. package/src/components/generic/GenericDashboard.jsx +93 -36
  22. package/src/components/generic/GenericDetail.jsx +146 -61
  23. package/src/components/generic/GenericIndex.jsx +134 -38
  24. package/src/components/generic/GenericSettings.jsx +220 -0
  25. package/src/components/generic/StandardModal.jsx +165 -8
  26. package/src/components/generic/dashboardChartDefaults.js +230 -0
  27. package/src/components/generic/shared/indexLayout.js +38 -0
  28. package/src/components/navActive.js +25 -0
  29. package/src/components/settings/IntegrationsManager.jsx +474 -0
  30. package/src/components/sms/SmsLineSettings.jsx +16 -12
  31. package/src/components/styles/AssociationManager.module.scss +4 -363
  32. package/src/components/styles/AuditLog.module.scss +6 -6
  33. package/src/components/styles/AuditLogs.module.scss +3 -3
  34. package/src/components/styles/Autocomplete.module.scss +7 -7
  35. package/src/components/styles/BusinessCardOcr.module.scss +6 -1
  36. package/src/components/styles/CallQueueSettings.module.scss +4 -1
  37. package/src/components/styles/CameraPlacement.module.scss +19 -19
  38. package/src/components/styles/CategorizedDropZone.module.scss +6 -1
  39. package/src/components/styles/ClientAssociationManager.module.scss +5 -289
  40. package/src/components/styles/ClientAuth.module.scss +1 -1
  41. package/src/components/styles/DataGrid.module.scss +455 -8
  42. package/src/components/styles/Field.module.scss +32 -24
  43. package/src/components/styles/Form.module.scss +123 -3
  44. package/src/components/styles/GenericDashboard.module.scss +612 -825
  45. package/src/components/styles/GenericDetail.module.scss +406 -61
  46. package/src/components/styles/GenericDynamic.module.scss +25 -25
  47. package/src/components/styles/GenericEditableTable.module.scss +33 -33
  48. package/src/components/styles/GenericFormBuilder.module.scss +119 -119
  49. package/src/components/styles/GenericIndex.module.scss +483 -38
  50. package/src/components/styles/GenericMain.module.scss +6 -3
  51. package/src/components/styles/GenericQuote.module.scss +399 -81
  52. package/src/components/styles/GenericReport.module.scss +8 -8
  53. package/src/components/styles/GenericSettings.module.scss +209 -0
  54. package/src/components/styles/GenericSort.module.scss +9 -9
  55. package/src/components/styles/Integrations.module.scss +321 -0
  56. package/src/components/styles/List.module.scss +4 -4
  57. package/src/components/styles/Login.module.scss +57 -0
  58. package/src/components/styles/MultiSelect.module.scss +4 -4
  59. package/src/components/styles/Navigation.module.scss +12 -12
  60. package/src/components/styles/Notification.module.scss +12 -8
  61. package/src/components/styles/NotificationList.module.scss +5 -5
  62. package/src/components/styles/Profile.module.scss +5 -5
  63. package/src/components/styles/QrCode.module.scss +4 -4
  64. package/src/components/styles/QuickAction.module.scss +20 -20
  65. package/src/components/styles/SortableQuoteItems.module.scss +4 -4
  66. package/src/components/styles/TableFilter.module.scss +128 -17
  67. package/src/components/styles/TicketConversation.module.scss +497 -0
  68. package/src/components/styles/UniversalSearch.module.scss +229 -0
  69. package/src/components/styles/Vault.module.scss +413 -0
  70. package/src/components/styles/_associationManager.scss +467 -0
  71. package/src/components/styles/_authBrandPanel.scss +15 -9
  72. package/src/components/styles/_controls.scss +43 -39
  73. package/src/components/styles/global-datagrid.css +196 -0
  74. package/src/components/styles/global.css +168 -17
  75. package/src/components/tickets/TicketConversation.jsx +455 -0
  76. package/src/components/utils/rowActionSettings.js +117 -0
  77. package/src/components/vault/VaultClientPicker.jsx +183 -0
  78. package/src/components/vault/VaultEntryForm.jsx +23 -0
  79. package/src/components/vault/VaultManager.jsx +382 -34
  80. package/src/components/vault/VaultShareModal.jsx +454 -0
  81. package/src/components/vault/vaultEndpoints.js +16 -2
  82. package/src/components/vault/vaultShares.js +247 -0
  83. package/src/index.js +35 -0
  84. package/src/utils/fetchUtil.js +38 -14
@@ -1,7 +1,9 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { toast } from 'react-toastify';
3
+ import { Star, Trash2, Plus, Calendar } from 'lucide-react';
3
4
  import CustomFetch from './Fetch';
4
5
  import MultiSelect from './MultiSelect';
6
+ import { confirmDialog } from './utils/ConfirmDialog';
5
7
  import styles from './styles/ClientAssociationManager.module.scss';
6
8
 
7
9
  const ClientAssociationManager = ({
@@ -13,7 +15,11 @@ const ClientAssociationManager = ({
13
15
  const [associations, setAssociations] = useState([]);
14
16
  const [availableOptions, setAvailableOptions] = useState([]);
15
17
  const [loading, setLoading] = useState(true);
16
-
18
+ /* The editor is the uncommon case — the common one is reading the list —
19
+ so the form sits behind an "Add" affordance rather than owning the foot
20
+ of the panel. */
21
+ const [showForm, setShowForm] = useState(false);
22
+
17
23
  // Dynamic state based on config
18
24
  const [newAssociation, setNewAssociation] = useState(() => {
19
25
  const initialState = {};
@@ -93,7 +99,7 @@ const ClientAssociationManager = ({
93
99
  // Generic action handler
94
100
  const handleAction = async (actionKey, data = {}) => {
95
101
  const endpoint = endpoints[actionKey];
96
- if (!endpoint) return;
102
+ if (!endpoint) return false;
97
103
 
98
104
  try {
99
105
  const url = processTemplate(endpoint.url || endpoint, data);
@@ -122,12 +128,16 @@ const ClientAssociationManager = ({
122
128
 
123
129
  loadAssociations();
124
130
  onUpdate();
131
+
132
+ return true;
125
133
  } catch (error) {
126
- const errorMessage = config.messages?.[`${actionKey}Error`] ||
127
- endpoint.errorMessage ||
134
+ const errorMessage = config.messages?.[`${actionKey}Error`] ||
135
+ endpoint.errorMessage ||
128
136
  'Action failed';
129
137
  toast.error(errorMessage);
130
138
  console.error(error);
139
+
140
+ return false;
131
141
  }
132
142
  };
133
143
 
@@ -153,7 +163,13 @@ const ClientAssociationManager = ({
153
163
  }
154
164
  });
155
165
 
156
- await handleAction('associate', submitData);
166
+ const ok = await handleAction('associate', submitData);
167
+
168
+ // Collapse only on success — a failed save keeps what was typed on
169
+ // screen so it can be corrected rather than retyped.
170
+ if (ok) {
171
+ setShowForm(false);
172
+ }
157
173
  };
158
174
 
159
175
  // Remove association handler
@@ -162,6 +178,24 @@ const ClientAssociationManager = ({
162
178
  await handleAction('dissociate', { [deleteField]: itemId });
163
179
  };
164
180
 
181
+ /* Removal is the one irreversible action on the panel, so it asks first —
182
+ the same `confirmDialog` every destructive control in the app uses. */
183
+ const confirmDissociate = (association) => {
184
+ const nameKey = config.displayFields?.name || 'name';
185
+
186
+ confirmDialog({
187
+ title: config.labels?.dissociate || config.labels?.remove || 'Remove',
188
+ message:
189
+ config.messages?.confirmDissociate ||
190
+ 'Are you sure you want to remove this association?',
191
+ buttons: [
192
+ { label: 'Yes', onClick: () => handleDissociate(association.id) },
193
+ { label: 'No', onClick: () => {} },
194
+ ],
195
+ data: { name: association[nameKey] },
196
+ });
197
+ };
198
+
165
199
  // Set primary handler
166
200
  const handleSetPrimary = async (itemId) => {
167
201
  if (!endpoints.setPrimary) return;
@@ -179,75 +213,286 @@ const ClientAssociationManager = ({
179
213
  }
180
214
  }, [dataId]);
181
215
 
216
+
217
+ /* Field names are resolved once, not per row. */
218
+ const nameField = config.displayFields?.name || 'name';
219
+ const primaryField = config.displayFields?.primary || 'is_primary';
220
+ const relationshipField = config.displayFields?.relationship || 'relationship_type';
221
+ const roleField = config.displayFields?.role || 'role';
222
+ const notesField = config.displayFields?.notes || 'notes';
223
+ const dateField = config.displayFields?.date || 'associated_at';
224
+
225
+ const requiredFieldId = config.fields?.find((f) => f.required)?.id || 'id';
226
+
227
+ /* The host page's tab already names this panel, so the title + subtitle
228
+ block that used to sit here is gone. A standalone mount can ask for a
229
+ compact one back with `config.showTitle`. */
230
+ const showTitle = Boolean(config.showTitle);
231
+
182
232
  if (loading) {
183
- return <div className={styles.loading}>{config.messages?.loading || 'Loading associations...'}</div>;
233
+ return (
234
+ <div className={styles.container}>
235
+ <p className={styles.loading}>
236
+ {config.messages?.loading || 'Loading associations...'}
237
+ </p>
238
+ </div>
239
+ );
184
240
  }
185
241
 
242
+ /* One quiet line of metadata under the name, joined by middots. Built as a
243
+ list so a missing field closes the gap rather than leaving a separator
244
+ stranded — the old markup printed a bare " - " before an absent note. */
245
+ const renderMeta = (association) => {
246
+ const parts = [];
247
+
248
+ if (association[relationshipField]) {
249
+ parts.push(
250
+ <span key="relationship" className={styles.relationshipType}>
251
+ {association[relationshipField]}
252
+ </span>
253
+ );
254
+ }
255
+
256
+ if (association[roleField]) {
257
+ parts.push(
258
+ <span key="role" className={styles.relationshipType}>
259
+ {association[roleField]}
260
+ </span>
261
+ );
262
+ }
263
+
264
+ if (association[notesField]) {
265
+ parts.push(
266
+ <span key="notes" className={styles.notes}>
267
+ {association[notesField]}
268
+ </span>
269
+ );
270
+ }
271
+
272
+ if (dateField && association[dateField]) {
273
+ parts.push(
274
+ <span key="date" className={styles.date}>
275
+ <Calendar size={11} aria-hidden="true" />
276
+ {config.labels?.associatedAt || 'Associated'}{' '}
277
+ {new Date(association[dateField]).toLocaleDateString()}
278
+ </span>
279
+ );
280
+ }
281
+
282
+ if (parts.length === 0) {
283
+ return null;
284
+ }
285
+
286
+ return (
287
+ <div className={styles.meta}>
288
+ {parts.map((part, index) => (
289
+ <React.Fragment key={part.key}>
290
+ {index > 0 && (
291
+ <span className={styles.sep} aria-hidden="true">
292
+ ·
293
+ </span>
294
+ )}
295
+ {part}
296
+ </React.Fragment>
297
+ ))}
298
+ </div>
299
+ );
300
+ };
301
+
302
+ const renderField = (field) => {
303
+ const fieldValue = newAssociation[field.id] || '';
304
+ const wide = field.type === 'textarea' || field.type === 'boolean';
305
+
306
+ if (field.type === 'boolean') {
307
+ return (
308
+ <div
309
+ key={field.id}
310
+ className={`${styles.field} ${styles.fieldWide}`}
311
+ >
312
+ <label className={styles.checkboxLabel}>
313
+ <input
314
+ type="checkbox"
315
+ checked={Boolean(fieldValue)}
316
+ onChange={(e) =>
317
+ setNewAssociation((prev) => ({
318
+ ...prev,
319
+ [field.id]: e.target.checked,
320
+ }))
321
+ }
322
+ />
323
+ {field.checkboxLabel || field.label}
324
+ </label>
325
+ </div>
326
+ );
327
+ }
328
+
329
+ return (
330
+ <div
331
+ key={field.id}
332
+ className={`${styles.field} ${wide ? styles.fieldWide : ''}`}
333
+ >
334
+ <label className={styles.label} htmlFor={`client-assoc-${field.id}`}>
335
+ {field.label}
336
+ {field.required && <span className={styles.required}> *</span>}
337
+ </label>
338
+
339
+ {field.type === 'multi-select' && (
340
+ <MultiSelect
341
+ settings={{ id: field.id }}
342
+ multi={field.multi || false}
343
+ onChange={(value) =>
344
+ setNewAssociation((prev) => ({ ...prev, [field.id]: value }))
345
+ }
346
+ inputValue={fieldValue}
347
+ options={availableOptions}
348
+ placeholder={field.placeholder || `Select ${field.label}...`}
349
+ />
350
+ )}
351
+
352
+ {field.type === 'select' && (
353
+ <select
354
+ id={`client-assoc-${field.id}`}
355
+ value={fieldValue}
356
+ onChange={(e) =>
357
+ setNewAssociation((prev) => ({
358
+ ...prev,
359
+ [field.id]: e.target.value,
360
+ }))
361
+ }
362
+ className={styles.selectField}
363
+ >
364
+ {field.options &&
365
+ field.options.map((option) => (
366
+ <option
367
+ key={option.value || option}
368
+ value={option.value || option}
369
+ >
370
+ {option.label || option}
371
+ </option>
372
+ ))}
373
+ </select>
374
+ )}
375
+
376
+ {field.type === 'textarea' && (
377
+ <textarea
378
+ id={`client-assoc-${field.id}`}
379
+ value={fieldValue}
380
+ onChange={(e) =>
381
+ setNewAssociation((prev) => ({
382
+ ...prev,
383
+ [field.id]: e.target.value,
384
+ }))
385
+ }
386
+ placeholder={field.placeholder || ''}
387
+ className={styles.textareaField}
388
+ rows={field.rows || 3}
389
+ />
390
+ )}
391
+
392
+ {field.type === 'text' && (
393
+ <input
394
+ id={`client-assoc-${field.id}`}
395
+ type="text"
396
+ value={fieldValue}
397
+ onChange={(e) =>
398
+ setNewAssociation((prev) => ({
399
+ ...prev,
400
+ [field.id]: e.target.value,
401
+ }))
402
+ }
403
+ placeholder={field.placeholder || ''}
404
+ className={styles.textField}
405
+ />
406
+ )}
407
+ </div>
408
+ );
409
+ };
410
+
186
411
  return (
187
412
  <div className={styles.container}>
188
- <div className={styles.header}>
189
- <h3>{config.title || 'Associations'}</h3>
190
- <p>{config.description || 'Manage associations'}</p>
191
- </div>
413
+ {showTitle && (
414
+ <div>
415
+ <h3 className={styles.heading}>{config.title || 'Associations'}</h3>
416
+ {config.description && (
417
+ <p className={styles.headingNote}>{config.description}</p>
418
+ )}
419
+ </div>
420
+ )}
421
+
422
+ <div className={styles.section}>
423
+ <div className={styles.sectionHead}>
424
+ <h4 className={styles.sectionTitle}>
425
+ {config.labels?.currentAssociations || 'Current Associations'}
426
+ </h4>
427
+ {!showForm && (
428
+ <button
429
+ type="button"
430
+ className={styles.addToggle}
431
+ onClick={() => setShowForm(true)}
432
+ >
433
+ <Plus size={14} aria-hidden="true" />
434
+ {config.labels?.addNew || 'Add Association'}
435
+ </button>
436
+ )}
437
+ </div>
192
438
 
193
- {/* Existing Associations */}
194
- <div className={styles.existingAssociations}>
195
- <h4>{config.labels?.currentAssociations || 'Current Associations'}</h4>
196
439
  {associations.length === 0 ? (
197
- <div className={styles.noAssociations}>
440
+ <p className={styles.empty}>
198
441
  {config.messages?.noAssociations || 'No associations found.'}
199
- </div>
442
+ </p>
200
443
  ) : (
201
- <div className={styles.associationsList}>
444
+ <div className={styles.list}>
202
445
  {associations.map((association) => {
203
- const nameField = config.displayFields?.name || 'name';
204
- const primaryField = config.displayFields?.primary || 'is_primary';
205
- const relationshipField = config.displayFields?.relationship || 'relationship_type';
206
- const notesField = config.displayFields?.notes || 'notes';
207
- const dateField = config.displayFields?.date || 'associated_at';
208
-
446
+ const isPrimary = Boolean(association[primaryField]);
447
+
209
448
  return (
210
- <div
211
- key={association.id}
212
- className={`${styles.associationItem} ${association[primaryField] ? styles.primary : ''}`}
449
+ <div
450
+ key={association.id}
451
+ className={`${styles.row} ${isPrimary ? styles.rowPrimary : ''}`}
213
452
  >
214
- <div className={styles.associationInfo}>
215
- <div className={styles.companyName}>
216
- {association[nameField]}
217
- {association[primaryField] && (
218
- <span className={styles.primaryBadge}>{config.labels?.primaryBadge || 'Primary'}</span>
219
- )}
220
- </div>
221
- <div className={styles.relationshipInfo}>
222
- <span className={styles.relationshipType}>
223
- {association[relationshipField]}
224
- </span>
225
- {association[notesField] && (
226
- <span className={styles.notes}>
227
- - {association[notesField]}
453
+ <div className={styles.main}>
454
+ <div className={styles.entityName}>
455
+ <span>{association[nameField]}</span>
456
+ {isPrimary && (
457
+ <span
458
+ className={`${styles.chip} ${styles.chipPrimary}`}
459
+ >
460
+ <Star
461
+ size={10}
462
+ fill="currentColor"
463
+ aria-hidden="true"
464
+ />
465
+ {config.labels?.primaryBadge || 'Primary'}
228
466
  </span>
229
467
  )}
230
468
  </div>
231
- <div className={styles.associatedAt}>
232
- {config.labels?.associatedAt || 'Associated'}: {new Date(association[dateField]).toLocaleDateString()}
233
- </div>
469
+ {renderMeta(association)}
234
470
  </div>
235
- <div className={styles.associationActions}>
236
- {!association[primaryField] && endpoints.setPrimary && (
471
+
472
+ <div className={styles.actions}>
473
+ {!isPrimary && endpoints.setPrimary && (
237
474
  <button
238
475
  type="button"
239
- className={styles.setPrimaryBtn}
476
+ className={styles.iconButton}
240
477
  onClick={() => handleSetPrimary(association.id)}
478
+ title={
479
+ config.labels?.setPrimary || 'Set Primary'
480
+ }
481
+ aria-label={
482
+ config.labels?.setPrimary || 'Set Primary'
483
+ }
241
484
  >
242
- {config.labels?.setPrimary || 'Set Primary'}
485
+ <Star size={15} aria-hidden="true" />
243
486
  </button>
244
487
  )}
245
488
  <button
246
489
  type="button"
247
- className={styles.removeBtn}
248
- onClick={() => handleDissociate(association.id)}
490
+ className={`${styles.iconButton} ${styles.iconButtonDanger}`}
491
+ onClick={() => confirmDissociate(association)}
492
+ title={config.labels?.remove || 'Remove'}
493
+ aria-label={config.labels?.remove || 'Remove'}
249
494
  >
250
- {config.labels?.remove || 'Remove'}
495
+ <Trash2 size={15} aria-hidden="true" />
251
496
  </button>
252
497
  </div>
253
498
  </div>
@@ -257,106 +502,37 @@ const ClientAssociationManager = ({
257
502
  )}
258
503
  </div>
259
504
 
260
- {/* Add New Association */}
261
- <div className={styles.newAssociation}>
262
- <h4>{config.labels?.addNew || 'Add New Association'}</h4>
263
- <div className={styles.associationForm}>
264
- {config.fields && config.fields.map((field, index) => {
265
- const fieldValue = newAssociation[field.id] || '';
266
-
267
- return (
268
- <div key={field.id} className={styles.formRow}>
269
- <div className={styles.formField}>
270
- <label>
271
- {field.label}{field.required && ' *'}
272
- </label>
273
-
274
- {field.type === 'multi-select' && (
275
- <MultiSelect
276
- settings={{ id: field.id }}
277
- multi={field.multi || false}
278
- onChange={(value) => setNewAssociation(prev => ({ ...prev, [field.id]: value }))}
279
- inputValue={fieldValue}
280
- options={availableOptions}
281
- placeholder={field.placeholder || `Select ${field.label}...`}
282
- />
283
- )}
284
-
285
- {field.type === 'select' && (
286
- <select
287
- value={fieldValue}
288
- onChange={(e) => setNewAssociation(prev => ({
289
- ...prev,
290
- [field.id]: e.target.value
291
- }))}
292
- className={styles.selectField}
293
- >
294
- {field.options && field.options.map(option => (
295
- <option key={option.value || option} value={option.value || option}>
296
- {option.label || option}
297
- </option>
298
- ))}
299
- </select>
300
- )}
301
-
302
- {field.type === 'boolean' && (
303
- <label>
304
- <input
305
- type="checkbox"
306
- checked={fieldValue}
307
- onChange={(e) => setNewAssociation(prev => ({
308
- ...prev,
309
- [field.id]: e.target.checked
310
- }))}
311
- />
312
- {field.checkboxLabel || field.label}
313
- </label>
314
- )}
315
-
316
- {field.type === 'textarea' && (
317
- <textarea
318
- value={fieldValue}
319
- onChange={(e) => setNewAssociation(prev => ({
320
- ...prev,
321
- [field.id]: e.target.value
322
- }))}
323
- placeholder={field.placeholder || ''}
324
- className={styles.textareaField}
325
- rows={field.rows || 3}
326
- />
327
- )}
328
-
329
- {field.type === 'text' && (
330
- <input
331
- type="text"
332
- value={fieldValue}
333
- onChange={(e) => setNewAssociation(prev => ({
334
- ...prev,
335
- [field.id]: e.target.value
336
- }))}
337
- placeholder={field.placeholder || ''}
338
- className={styles.textField}
339
- />
340
- )}
341
- </div>
342
- </div>
343
- );
344
- })}
505
+ {showForm && (
506
+ <div className={styles.section}>
507
+ <h4 className={styles.sectionTitle}>
508
+ {config.labels?.addNew || 'Add New Association'}
509
+ </h4>
510
+ <div className={styles.form}>
511
+ {config.fields && config.fields.map(renderField)}
345
512
 
346
- <div className={styles.formActions}>
347
- <button
348
- type="button"
349
- className={styles.associateBtn}
350
- onClick={handleAssociate}
351
- disabled={!newAssociation[config.fields?.find(f => f.required)?.id || 'id']}
352
- >
353
- {config.labels?.addButton || 'Add Association'}
354
- </button>
513
+ <div className={styles.formActions}>
514
+ <button
515
+ type="button"
516
+ className={styles.secondaryButton}
517
+ onClick={() => setShowForm(false)}
518
+ >
519
+ {config.labels?.cancel || 'Cancel'}
520
+ </button>
521
+ <button
522
+ type="button"
523
+ className={styles.primaryButton}
524
+ onClick={handleAssociate}
525
+ disabled={!newAssociation[requiredFieldId]}
526
+ >
527
+ <Plus size={14} aria-hidden="true" />
528
+ {config.labels?.addButton || 'Add Association'}
529
+ </button>
530
+ </div>
355
531
  </div>
356
532
  </div>
357
- </div>
533
+ )}
358
534
  </div>
359
535
  );
360
536
  };
361
537
 
362
- export default ClientAssociationManager;
538
+ export default ClientAssociationManager;