bison-web-components 1.0.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.
@@ -0,0 +1,1473 @@
1
+ /**
2
+ * OperatorUnderwriting Web Component
3
+ *
4
+ * A web component for operator underwriting with a button that opens a modal.
5
+ * Validates operator email via API and manages button state based on response.
6
+ *
7
+ * @author @kfajardo
8
+ * @version 1.0.0
9
+ *
10
+ * @requires BisonJibPayAPI - Must be loaded before this component (from api.js)
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <script type="module" src="component.js"></script>
15
+ *
16
+ * <operator-underwriting
17
+ * operator-email="operator@example.com"
18
+ * api-base-url="https://api.example.com"
19
+ * embeddable-key="your-key">
20
+ * </operator-underwriting>
21
+ *
22
+ * <script>
23
+ * const underwriting = document.querySelector('operator-underwriting');
24
+ * underwriting.addEventListener('underwriting-ready', (e) => {
25
+ * console.log('Account validated:', e.detail.moovAccountId);
26
+ * });
27
+ * underwriting.addEventListener('underwriting-error', (e) => {
28
+ * console.error('Validation failed:', e.detail.error);
29
+ * });
30
+ * </script>
31
+ * ```
32
+ */
33
+
34
+ class OperatorUnderwriting extends HTMLElement {
35
+ constructor() {
36
+ super();
37
+ this.attachShadow({ mode: "open" });
38
+
39
+ // API Configuration
40
+ this.apiBaseURL =
41
+ this.getAttribute("api-base-url") ||
42
+ "https://bison-jib-development.azurewebsites.net";
43
+ this.embeddableKey =
44
+ this.getAttribute("embeddable-key") ||
45
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
46
+
47
+ // Check if BisonJibPayAPI is available
48
+ if (typeof BisonJibPayAPI === "undefined") {
49
+ console.error(
50
+ "OperatorUnderwriting: BisonJibPayAPI is not available. Please ensure api.js is loaded before operator-underwriting.js"
51
+ );
52
+ this.api = null;
53
+ } else {
54
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
55
+ }
56
+
57
+ // Internal state
58
+ this._state = {
59
+ operatorEmail: null,
60
+ moovAccountId: null,
61
+ isLoading: false,
62
+ isError: false,
63
+ isModalOpen: false,
64
+ error: null,
65
+ underwritingHistory: null,
66
+ isLoadingUnderwritingHistory: false,
67
+ underwritingHistoryError: null,
68
+ hasInitialized: false, // Guard to prevent multiple initializations
69
+ };
70
+
71
+ // Render the component
72
+ this.render();
73
+ }
74
+
75
+ // ==================== STATIC PROPERTIES ====================
76
+
77
+ static get observedAttributes() {
78
+ return ["operator-email", "api-base-url", "embeddable-key"];
79
+ }
80
+
81
+ // ==================== PROPERTY GETTERS/SETTERS ====================
82
+
83
+ /**
84
+ * Get the operator email
85
+ * @returns {string|null}
86
+ */
87
+ get operatorEmail() {
88
+ return this._state.operatorEmail;
89
+ }
90
+
91
+ /**
92
+ * Set the operator email
93
+ * @param {string} value - Operator email address
94
+ */
95
+ set operatorEmail(value) {
96
+ console.log("OperatorUnderwriting: Setting operator email to:", value);
97
+
98
+ const oldEmail = this._state.operatorEmail;
99
+
100
+ // Update internal state
101
+ this._state.operatorEmail = value;
102
+
103
+ // Update attribute only if different to prevent circular updates
104
+ const currentAttr = this.getAttribute("operator-email");
105
+ if (currentAttr !== value) {
106
+ if (value) {
107
+ this.setAttribute("operator-email", value);
108
+ } else {
109
+ this.removeAttribute("operator-email");
110
+ }
111
+ }
112
+
113
+ // Trigger initialization if email changed and component is connected
114
+ if (value && value !== oldEmail && this.isConnected) {
115
+ this.initializeAccount();
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get the moov account ID
121
+ * @returns {string|null}
122
+ */
123
+ get moovAccountId() {
124
+ return this._state.moovAccountId;
125
+ }
126
+
127
+ /**
128
+ * Check if the component is ready (account validated)
129
+ * @returns {boolean}
130
+ */
131
+ get isReady() {
132
+ return (
133
+ !this._state.isLoading &&
134
+ !this._state.isError &&
135
+ !!this._state.moovAccountId
136
+ );
137
+ }
138
+
139
+ /**
140
+ * Get the open state
141
+ * @returns {boolean}
142
+ */
143
+ get isOpen() {
144
+ return this._state.isModalOpen;
145
+ }
146
+
147
+ // ==================== LIFECYCLE METHODS ====================
148
+
149
+ connectedCallback() {
150
+ // Initialize email from attribute if present
151
+ const emailAttr = this.getAttribute("operator-email");
152
+ if (emailAttr && !this._state.operatorEmail) {
153
+ this._state.operatorEmail = emailAttr;
154
+ // Trigger API call when email is set
155
+ this.initializeAccount();
156
+ }
157
+
158
+ this.setupEventListeners();
159
+ }
160
+
161
+ disconnectedCallback() {
162
+ this.removeEventListeners();
163
+ }
164
+
165
+ attributeChangedCallback(name, oldValue, newValue) {
166
+ if (oldValue === newValue) return;
167
+
168
+ switch (name) {
169
+ case "operator-email":
170
+ console.log(
171
+ "OperatorUnderwriting: attributeChangedCallback - operator-email:",
172
+ newValue
173
+ );
174
+ this._state.operatorEmail = newValue;
175
+ // Reset state when email changes
176
+ this._state.moovAccountId = null;
177
+ this._state.isError = false;
178
+ this._state.error = null;
179
+ this._state.hasInitialized = false; // Allow re-initialization for new email
180
+ // Trigger API call
181
+ if (newValue && this.isConnected) {
182
+ this.initializeAccount();
183
+ }
184
+ break;
185
+
186
+ case "api-base-url":
187
+ this.apiBaseURL = newValue;
188
+ if (typeof BisonJibPayAPI !== "undefined") {
189
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
190
+ }
191
+ break;
192
+
193
+ case "embeddable-key":
194
+ this.embeddableKey = newValue;
195
+ if (typeof BisonJibPayAPI !== "undefined") {
196
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
197
+ }
198
+ break;
199
+ }
200
+ }
201
+
202
+ // ==================== API INTEGRATION ====================
203
+
204
+ /**
205
+ * Initialize account by calling getAccountByEmail API
206
+ * Called when operator-email attribute is set
207
+ */
208
+ async initializeAccount() {
209
+ // Validate email is set
210
+ if (!this._state.operatorEmail) {
211
+ console.warn(
212
+ "OperatorUnderwriting: Email is required for initialization"
213
+ );
214
+ return;
215
+ }
216
+
217
+ // Prevent multiple simultaneous initializations
218
+ if (this._state.isLoading || this._state.hasInitialized) {
219
+ console.log(
220
+ "OperatorUnderwriting: Already initializing or initialized, skipping"
221
+ );
222
+ return;
223
+ }
224
+
225
+ // Validate API is available
226
+ if (!this.api) {
227
+ console.error(
228
+ "OperatorUnderwriting: BisonJibPayAPI is not available. Please ensure api.js is loaded first."
229
+ );
230
+ this._state.isError = true;
231
+ this._state.error = "API not available";
232
+ this._state.hasInitialized = true;
233
+ this.updateButtonState();
234
+ return;
235
+ }
236
+
237
+ // Set loading state
238
+ this._state.isLoading = true;
239
+ this._state.isError = false;
240
+ this._state.error = null;
241
+ this.updateButtonState();
242
+
243
+ try {
244
+ console.log(
245
+ "OperatorUnderwriting: Calling getAccountByEmail for:",
246
+ this._state.operatorEmail
247
+ );
248
+
249
+ const response = await this.api.getAccountByEmail(
250
+ this._state.operatorEmail
251
+ );
252
+
253
+ // Success: Store moov account ID
254
+ this._state.moovAccountId =
255
+ response.data?.moovAccountId || response.moovAccountId;
256
+ this._state.isLoading = false;
257
+ this._state.isError = false;
258
+ this._state.hasInitialized = true;
259
+
260
+ console.log(
261
+ "OperatorUnderwriting: Account validated, moovAccountId:",
262
+ this._state.moovAccountId
263
+ );
264
+
265
+ // Emit success event
266
+ this.dispatchEvent(
267
+ new CustomEvent("underwriting-ready", {
268
+ detail: {
269
+ moovAccountId: this._state.moovAccountId,
270
+ operatorEmail: this._state.operatorEmail,
271
+ },
272
+ bubbles: true,
273
+ composed: true,
274
+ })
275
+ );
276
+ } catch (error) {
277
+ // Failure: Set error state
278
+ this._state.isError = true;
279
+ this._state.isLoading = false;
280
+ this._state.moovAccountId = null;
281
+ this._state.hasInitialized = true;
282
+ this._state.error =
283
+ error.data?.message || error.message || "Failed to validate operator";
284
+
285
+ console.error("OperatorUnderwriting: API call failed:", error);
286
+
287
+ // Emit error event
288
+ this.dispatchEvent(
289
+ new CustomEvent("underwriting-error", {
290
+ detail: {
291
+ error: this._state.error,
292
+ operatorEmail: this._state.operatorEmail,
293
+ originalError: error,
294
+ },
295
+ bubbles: true,
296
+ composed: true,
297
+ })
298
+ );
299
+ }
300
+
301
+ this.updateButtonState();
302
+ }
303
+
304
+ // ==================== EVENT HANDLING ====================
305
+
306
+ setupEventListeners() {
307
+ const button = this.shadowRoot.querySelector(".underwriting-btn");
308
+ const closeBtn = this.shadowRoot.querySelector(".close-btn");
309
+ const overlay = this.shadowRoot.querySelector(".modal-overlay");
310
+
311
+ if (button) {
312
+ button.addEventListener("click", this.handleButtonClick.bind(this));
313
+ }
314
+
315
+ if (closeBtn) {
316
+ closeBtn.addEventListener("click", this.closeModal.bind(this));
317
+ }
318
+
319
+ if (overlay) {
320
+ overlay.addEventListener("click", this.closeModal.bind(this));
321
+ }
322
+
323
+ // ESC key to close modal
324
+ this._escHandler = (e) => {
325
+ if (e.key === "Escape" && this._state.isModalOpen) {
326
+ this.closeModal();
327
+ }
328
+ };
329
+ document.addEventListener("keydown", this._escHandler);
330
+ }
331
+
332
+ removeEventListeners() {
333
+ if (this._escHandler) {
334
+ document.removeEventListener("keydown", this._escHandler);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Handle button click - open modal if ready
340
+ */
341
+ handleButtonClick() {
342
+ console.log("OperatorUnderwriting: Button clicked");
343
+
344
+ // Only open modal if not loading and not in error state
345
+ if (this._state.isLoading || this._state.isError) {
346
+ console.warn(
347
+ "OperatorUnderwriting: Cannot open modal - button is disabled"
348
+ );
349
+ return;
350
+ }
351
+
352
+ // Validate we have an account ID
353
+ if (!this._state.moovAccountId) {
354
+ console.warn("OperatorUnderwriting: Cannot open modal - no account ID");
355
+ return;
356
+ }
357
+
358
+ this.openModal();
359
+ }
360
+
361
+ /**
362
+ * Open the modal
363
+ */
364
+ async openModal() {
365
+ this._state.isModalOpen = true;
366
+ const modal = this.shadowRoot.querySelector(".modal");
367
+ if (modal) {
368
+ // Show modal and start animation
369
+ modal.classList.add("show", "animating-in");
370
+
371
+ // Remove animating-in class after animation completes
372
+ setTimeout(() => {
373
+ modal.classList.remove("animating-in");
374
+ }, 200);
375
+ }
376
+
377
+ // Prevent background scrolling when modal is open
378
+ document.body.style.overflow = "hidden";
379
+
380
+ // Fetch underwriting history when modal opens
381
+ await this.fetchUnderwritingHistory();
382
+
383
+ // Emit modal open event
384
+ this.dispatchEvent(
385
+ new CustomEvent("underwriting-modal-open", {
386
+ detail: {
387
+ moovAccountId: this._state.moovAccountId,
388
+ operatorEmail: this._state.operatorEmail,
389
+ },
390
+ bubbles: true,
391
+ composed: true,
392
+ })
393
+ );
394
+ }
395
+
396
+ /**
397
+ * Close the modal
398
+ */
399
+ closeModal() {
400
+ this._state.isModalOpen = false;
401
+ const modal = this.shadowRoot.querySelector(".modal");
402
+ if (modal) {
403
+ // Start close animation
404
+ modal.classList.add("animating-out");
405
+
406
+ // Hide modal after animation completes
407
+ setTimeout(() => {
408
+ modal.classList.remove("show", "animating-out");
409
+ }, 150);
410
+ }
411
+
412
+ // Restore background scrolling when modal is closed
413
+ document.body.style.overflow = "";
414
+
415
+ // Emit modal close event
416
+ this.dispatchEvent(
417
+ new CustomEvent("underwriting-modal-close", {
418
+ detail: {
419
+ moovAccountId: this._state.moovAccountId,
420
+ operatorEmail: this._state.operatorEmail,
421
+ },
422
+ bubbles: true,
423
+ composed: true,
424
+ })
425
+ );
426
+ }
427
+
428
+ /**
429
+ * Fetch underwriting history using the saved moovAccountId
430
+ */
431
+ async fetchUnderwritingHistory() {
432
+ // Validate we have a moovAccountId
433
+ if (!this._state.moovAccountId) {
434
+ console.warn(
435
+ "OperatorUnderwriting: Cannot fetch underwriting history - no moovAccountId"
436
+ );
437
+ return;
438
+ }
439
+
440
+ // Validate API is available
441
+ if (!this.api) {
442
+ console.error(
443
+ "OperatorUnderwriting: BisonJibPayAPI is not available for fetching underwriting history"
444
+ );
445
+ return;
446
+ }
447
+
448
+ // Set loading state
449
+ this._state.isLoadingUnderwritingHistory = true;
450
+ this._state.underwritingHistoryError = null;
451
+ this.updateModalContent();
452
+
453
+ try {
454
+ console.log(
455
+ "OperatorUnderwriting: Fetching underwriting history for moovAccountId:",
456
+ this._state.moovAccountId
457
+ );
458
+
459
+ const response = await this.api.fetchUnderwritingByAccountId(
460
+ this._state.moovAccountId
461
+ );
462
+
463
+ // Success: Store underwriting history
464
+ this._state.underwritingHistory = response.data || [];
465
+ this._state.isLoadingUnderwritingHistory = false;
466
+
467
+ console.log(
468
+ "OperatorUnderwriting: Underwriting history fetched successfully:",
469
+ this._state.underwritingHistory
470
+ );
471
+
472
+ // Emit success event
473
+ this.dispatchEvent(
474
+ new CustomEvent("underwriting-history-loaded", {
475
+ detail: {
476
+ moovAccountId: this._state.moovAccountId,
477
+ history: this._state.underwritingHistory,
478
+ },
479
+ bubbles: true,
480
+ composed: true,
481
+ })
482
+ );
483
+ } catch (error) {
484
+ // Failure: Set error state
485
+ this._state.isLoadingUnderwritingHistory = false;
486
+ this._state.underwritingHistoryError =
487
+ error.data?.message ||
488
+ error.message ||
489
+ "Failed to load underwriting history";
490
+
491
+ console.error(
492
+ "OperatorUnderwriting: Failed to fetch underwriting history:",
493
+ error
494
+ );
495
+
496
+ // Emit error event
497
+ this.dispatchEvent(
498
+ new CustomEvent("underwriting-history-error", {
499
+ detail: {
500
+ error: this._state.underwritingHistoryError,
501
+ moovAccountId: this._state.moovAccountId,
502
+ originalError: error,
503
+ },
504
+ bubbles: true,
505
+ composed: true,
506
+ })
507
+ );
508
+ }
509
+
510
+ this.updateModalContent();
511
+ }
512
+
513
+ /**
514
+ * Update modal content based on underwriting history state
515
+ */
516
+ updateModalContent() {
517
+ const modalBody = this.shadowRoot.querySelector(".modal-body");
518
+ if (!modalBody) return;
519
+
520
+ if (this._state.isLoadingUnderwritingHistory) {
521
+ modalBody.innerHTML = `
522
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center;">
523
+ <div style="width: 48px; height: 48px; border: 4px solid var(--color-border, #e8e8e8); border-top-color: var(--color-primary, #4c7b63); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 16px;"></div>
524
+ <p style="font-size: 16px; color: var(--color-gray-500, #6b7280); margin: 0;">Loading underwriting history...</p>
525
+ </div>
526
+ `;
527
+ } else if (this._state.underwritingHistoryError) {
528
+ modalBody.innerHTML = `
529
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; background: var(--color-error-light, #fae5e4); border: 1px solid var(--color-error-muted, #eea9a5); border-radius: var(--radius-xl, 0.75rem);">
530
+ <svg style="width: 64px; height: 64px; color: var(--color-error, #dd524b); margin-bottom: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
531
+ <circle cx="12" cy="12" r="10"></circle>
532
+ <line x1="12" y1="8" x2="12" y2="12"></line>
533
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
534
+ </svg>
535
+ <p style="font-size: 16px; color: var(--color-error-dark, #903531); margin: 0;">Failed to load underwriting history</p>
536
+ <p style="font-size: 14px; color: var(--color-error, #dd524b); margin-top: 8px;">${this._state.underwritingHistoryError}</p>
537
+ </div>
538
+ `;
539
+ } else if (
540
+ this._state.underwritingHistory &&
541
+ this._state.underwritingHistory.length > 0
542
+ ) {
543
+ // Log underwriting data when available
544
+ console.log(
545
+ "OperatorUnderwriting: Underwriting data:",
546
+ this._state.underwritingHistory
547
+ );
548
+
549
+ modalBody.innerHTML = this.renderUnderwritingTimeline(
550
+ this._state.underwritingHistory
551
+ );
552
+ } else {
553
+ modalBody.innerHTML = `
554
+ <div style="display: flex; margin: 24px 0; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; background: var(--color-gray-50, #f9fafb); border: 1px dashed var(--color-gray-200, #d1d5db); border-radius: var(--radius-xl, 0.75rem);">
555
+ <svg style="width: 64px; height: 64px; color: var(--color-gray-400, #9ca3af); margin-bottom: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
556
+ <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
557
+ </svg>
558
+ <p style="font-size: 16px; color: var(--color-gray-500, #6b7280); margin: 0;">No underwriting history found</p>
559
+ <p style="font-size: 14px; color: var(--color-gray-400, #9ca3af); margin-top: 8px;">This operator has no underwriting records yet</p>
560
+ </div>
561
+ `;
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Update button state based on current state
567
+ */
568
+ updateButtonState() {
569
+ const button = this.shadowRoot.querySelector(".underwriting-btn");
570
+ const wrapper = this.shadowRoot.querySelector(".btn-wrapper");
571
+ if (!button) return;
572
+
573
+ // Remove all state classes
574
+ button.classList.remove("loading", "error");
575
+
576
+ if (this._state.isLoading) {
577
+ button.classList.add("loading");
578
+ button.disabled = true;
579
+ if (wrapper) wrapper.classList.remove("has-error");
580
+ } else if (this._state.isError) {
581
+ button.classList.add("error");
582
+ button.disabled = true;
583
+ if (wrapper) wrapper.classList.add("has-error");
584
+ } else {
585
+ button.disabled = false;
586
+ if (wrapper) wrapper.classList.remove("has-error");
587
+ }
588
+ }
589
+
590
+ // ==================== RENDERING ====================
591
+
592
+ /**
593
+ * Render the component (Shadow DOM)
594
+ */
595
+ render() {
596
+ this.shadowRoot.innerHTML = `
597
+ <style>
598
+ :host {
599
+ display: inline-block;
600
+ font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
601
+ color: var(--color-secondary, #5f6e78);
602
+ }
603
+
604
+ .underwriting-btn {
605
+ padding: 12px 24px;
606
+ background: var(--color-primary, #4c7b63);
607
+ color: var(--color-white, #fff);
608
+ border: none;
609
+ border-radius: var(--radius-xl, 0.75rem);
610
+ font-size: var(--text-sm, 0.875rem);
611
+ font-weight: var(--font-weight-medium, 500);
612
+ cursor: pointer;
613
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
614
+ display: inline-flex;
615
+ align-items: center;
616
+ gap: 8px;
617
+ height: 40px;
618
+ box-sizing: border-box;
619
+ }
620
+
621
+ .underwriting-btn:hover:not(.error):not(.loading):not(:disabled) {
622
+ background: var(--color-primary-hover, #436c57);
623
+ }
624
+
625
+ .underwriting-btn:active:not(.error):not(.loading):not(:disabled) {
626
+ background: var(--color-primary-active, #3d624f);
627
+ }
628
+
629
+ .underwriting-btn.error {
630
+ background: var(--color-gray-400, #9ca3af);
631
+ cursor: not-allowed;
632
+ }
633
+
634
+ .underwriting-btn.loading {
635
+ background: var(--color-primary-soft, #678f7a);
636
+ cursor: wait;
637
+ }
638
+
639
+ .underwriting-btn .broken-link-icon {
640
+ display: none;
641
+ }
642
+
643
+ .underwriting-btn.error .broken-link-icon {
644
+ display: inline-block;
645
+ }
646
+
647
+ .underwriting-btn .loading-spinner {
648
+ display: none;
649
+ width: 16px;
650
+ height: 16px;
651
+ border: 2px solid rgba(255, 255, 255, 0.3);
652
+ border-top-color: var(--color-white, #fff);
653
+ border-radius: 50%;
654
+ animation: spin 0.8s linear infinite;
655
+ box-sizing: border-box;
656
+ }
657
+
658
+ .underwriting-btn.loading .loading-spinner {
659
+ display: inline-block;
660
+ }
661
+
662
+ @keyframes spin {
663
+ to {
664
+ transform: rotate(360deg);
665
+ }
666
+ }
667
+
668
+ .btn-wrapper {
669
+ position: relative;
670
+ display: inline-block;
671
+ }
672
+
673
+ .tooltip {
674
+ visibility: hidden;
675
+ opacity: 0;
676
+ position: absolute;
677
+ bottom: 100%;
678
+ left: 50%;
679
+ transform: translateX(-50%);
680
+ background: var(--color-gray-700, #374151);
681
+ color: var(--color-white, #fff);
682
+ padding: 8px 12px;
683
+ border-radius: var(--radius-lg, 0.5rem);
684
+ font-size: 13px;
685
+ white-space: nowrap;
686
+ margin-bottom: 8px;
687
+ transition: opacity var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1)),
688
+ visibility var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
689
+ z-index: 10002;
690
+ }
691
+
692
+ .tooltip::after {
693
+ content: '';
694
+ position: absolute;
695
+ top: 100%;
696
+ left: 50%;
697
+ transform: translateX(-50%);
698
+ border: 6px solid transparent;
699
+ border-top-color: var(--color-gray-700, #374151);
700
+ }
701
+
702
+ .btn-wrapper:hover .tooltip {
703
+ visibility: visible;
704
+ opacity: 1;
705
+ }
706
+
707
+ .btn-wrapper:not(.has-error) .tooltip {
708
+ display: none;
709
+ }
710
+
711
+ .modal {
712
+ display: none;
713
+ position: fixed;
714
+ top: 0;
715
+ left: 0;
716
+ right: 0;
717
+ bottom: 0;
718
+ z-index: 10000;
719
+ align-items: center;
720
+ justify-content: center;
721
+ }
722
+
723
+ .modal.show {
724
+ display: flex;
725
+ }
726
+
727
+ .modal.animating-in .modal-overlay {
728
+ animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
729
+ }
730
+
731
+ .modal.animating-in .modal-content {
732
+ animation: slideInScale 0.2s cubic-bezier(0.4, 0, 0.2, 1);
733
+ }
734
+
735
+ .modal.animating-out .modal-overlay {
736
+ animation: fadeOut 0.15s cubic-bezier(0.4, 0, 1, 1);
737
+ }
738
+
739
+ .modal.animating-out .modal-content {
740
+ animation: slideOutScale 0.15s cubic-bezier(0.4, 0, 1, 1);
741
+ }
742
+
743
+ @keyframes fadeIn {
744
+ from {
745
+ opacity: 0;
746
+ }
747
+ to {
748
+ opacity: 1;
749
+ }
750
+ }
751
+
752
+ @keyframes fadeOut {
753
+ from {
754
+ opacity: 1;
755
+ }
756
+ to {
757
+ opacity: 0;
758
+ }
759
+ }
760
+
761
+ @keyframes slideInScale {
762
+ from {
763
+ opacity: 0;
764
+ transform: scale(0.95) translateY(-10px);
765
+ }
766
+ to {
767
+ opacity: 1;
768
+ transform: scale(1) translateY(0);
769
+ }
770
+ }
771
+
772
+ @keyframes slideOutScale {
773
+ from {
774
+ opacity: 1;
775
+ transform: scale(1) translateY(0);
776
+ }
777
+ to {
778
+ opacity: 0;
779
+ transform: scale(0.98) translateY(-8px);
780
+ }
781
+ }
782
+
783
+ .modal-overlay {
784
+ position: absolute;
785
+ top: 0;
786
+ left: 0;
787
+ right: 0;
788
+ bottom: 0;
789
+ background: rgba(0, 0, 0, 0.5);
790
+ }
791
+
792
+ .modal-content {
793
+ position: relative;
794
+ background: var(--color-white, #fff);
795
+ border-radius: var(--radius-xl, 0.75rem);
796
+ width: 90%;
797
+ max-width: 600px;
798
+ height: 80vh;
799
+ max-height: 600px;
800
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
801
+ z-index: 10001;
802
+ display: flex;
803
+ flex-direction: column;
804
+ padding: 40px;
805
+ overflow: hidden;
806
+ }
807
+
808
+ .close-btn {
809
+ position: absolute;
810
+ top: 16px;
811
+ right: 16px;
812
+ background: transparent;
813
+ border: none;
814
+ font-size: 28px;
815
+ color: var(--color-gray-400, #9ca3af);
816
+ cursor: pointer;
817
+ width: 32px;
818
+ height: 32px;
819
+ display: flex;
820
+ align-items: center;
821
+ justify-content: center;
822
+ border-radius: 4px;
823
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
824
+ }
825
+
826
+ .close-btn:hover {
827
+ background: var(--color-gray-100, #f3f4f6);
828
+ color: var(--color-headline, #0f2a39);
829
+ }
830
+
831
+ /* Modal Header - Static */
832
+ .modal-header {
833
+ text-align: center;
834
+ padding-bottom: 16px;
835
+ border-bottom: 1px solid var(--color-border, #e8e8e8);
836
+ flex-shrink: 0;
837
+ }
838
+
839
+ .modal-header h2 {
840
+ font-size: 20px;
841
+ font-weight: var(--font-weight-semibold, 600);
842
+ color: var(--color-headline, #0f2a39);
843
+ margin: 0 0 4px 0;
844
+ }
845
+
846
+ .modal-header p {
847
+ font-size: 14px;
848
+ color: var(--color-gray-500, #6b7280);
849
+ margin: 0;
850
+ }
851
+
852
+ /* Modal Body */
853
+ .modal-body {
854
+ width: 100%;
855
+ flex: 1;
856
+ overflow-y: auto;
857
+ min-height: 0;
858
+ }
859
+
860
+
861
+ /* Powered By Footer - Static */
862
+ .powered-by {
863
+ display: flex;
864
+ align-items: center;
865
+ justify-content: center;
866
+ gap: 6px;
867
+ padding-top: 16px;
868
+ border-top: 1px solid var(--color-border, #e8e8e8);
869
+ font-size: 11px;
870
+ color: var(--color-gray-400, #9ca3af);
871
+ flex-shrink: 0;
872
+ }
873
+
874
+ .powered-by svg {
875
+ width: 16px;
876
+ height: 16px;
877
+ }
878
+
879
+ .powered-by span {
880
+ font-weight: 500;
881
+ color: var(--color-gray-500, #6b7280);
882
+ }
883
+ </style>
884
+
885
+ <!-- Main Button -->
886
+ <div class="btn-wrapper">
887
+ <span class="tooltip">Operator is not onboarded to the Bison system</span>
888
+ <button class="underwriting-btn">
889
+ <span class="loading-spinner"></span>
890
+ <svg class="broken-link-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
891
+ <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
892
+ <line x1="1" y1="1" x2="23" y2="23"></line>
893
+ </svg>
894
+ View Underwriting Status
895
+ </button>
896
+ </div>
897
+
898
+ <!-- Modal -->
899
+ <div class="modal">
900
+ <div class="modal-overlay"></div>
901
+ <div class="modal-content">
902
+ <button class="close-btn">×</button>
903
+
904
+ <!-- Modal Header -->
905
+ <div class="modal-header">
906
+ <h2>Underwriting Status</h2>
907
+ <p>Track your underwriting application progress</p>
908
+ </div>
909
+
910
+ <!-- Content Area -->
911
+ <div class="modal-body">
912
+ </div>
913
+
914
+ <!-- Powered By Footer -->
915
+ <div class="powered-by">
916
+ Powered by
917
+ <img src="./bison_logo.png" alt="Bison" style="height: 16px; margin-left: 4px;" onerror="this.onerror=null; this.src='https://bisonpaywell.com/lovable-uploads/28831244-e8b3-4e7b-8dbb-c016f9f9d54f.png';">
918
+ </div>
919
+ </div>
920
+ </div>
921
+ `;
922
+ }
923
+
924
+ /**
925
+ * Render the timeline with payment methods
926
+ * @returns {string} HTML string for timeline
927
+ */
928
+ renderTimeline() {
929
+ // Show loading state
930
+ if (this._state.isLoadingPaymentMethods) {
931
+ return `
932
+ <div class="timeline-loading">
933
+ <div class="loading-spinner-large"></div>
934
+ <p>Loading payment methods...</p>
935
+ </div>
936
+ `;
937
+ }
938
+
939
+ // Show error state
940
+ if (this._state.paymentMethodsError) {
941
+ return `
942
+ <div class="timeline-error">
943
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
944
+ <circle cx="12" cy="12" r="10"></circle>
945
+ <line x1="12" y1="8" x2="12" y2="12"></line>
946
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
947
+ </svg>
948
+ <p>${this._state.paymentMethodsError}</p>
949
+ <button class="retry-btn">Retry</button>
950
+ </div>
951
+ `;
952
+ }
953
+
954
+ // Show empty state
955
+ if (
956
+ !this._state.paymentMethods ||
957
+ this._state.paymentMethods.length === 0
958
+ ) {
959
+ return `
960
+ <div class="timeline-empty">
961
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
962
+ <rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
963
+ <line x1="1" y1="10" x2="23" y2="10"></line>
964
+ </svg>
965
+ <p>No payment methods linked yet</p>
966
+ </div>
967
+ `;
968
+ }
969
+
970
+ // Render payment methods as timeline items
971
+ return this._state.paymentMethods
972
+ .map((method) => this.renderPaymentMethodItem(method))
973
+ .join("");
974
+ }
975
+
976
+ /**
977
+ * Render a single payment method as timeline item
978
+ * @param {Object} method - Payment method data
979
+ * @returns {string} HTML string for timeline item
980
+ */
981
+ renderPaymentMethodItem(method) {
982
+ const icon = this.getPaymentMethodIcon(method.paymentMethodType);
983
+ const title = this.formatPaymentMethodTitle(method);
984
+ const description = this.formatPaymentMethodDescription(method);
985
+ const paymentMethodId = method.paymentMethodID;
986
+
987
+ return `
988
+ <div class="timeline-item">
989
+ <div class="timeline-icon completed">
990
+ ${this.getPaymentTypeIconSvg(method.paymentMethodType)}
991
+ </div>
992
+ <div class="timeline-card">
993
+ <div class="card-header">
994
+ <h4 class="card-title">
995
+ <span class="payment-type-icon">${icon}</span>
996
+ ${title}
997
+ </h4>
998
+ <button class="delete-btn" data-payment-method-id="${paymentMethodId}" title="Delete payment method">
999
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1000
+ <polyline points="3 6 5 6 21 6"></polyline>
1001
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
1002
+ <line x1="10" y1="11" x2="10" y2="17"></line>
1003
+ <line x1="14" y1="11" x2="14" y2="17"></line>
1004
+ </svg>
1005
+ </button>
1006
+ </div>
1007
+ <p class="card-description">${description}</p>
1008
+ <div class="card-timestamp">
1009
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1010
+ <circle cx="12" cy="12" r="10"></circle>
1011
+ <polyline points="12 6 12 12 16 14"></polyline>
1012
+ </svg>
1013
+ ${this.formatPaymentMethodTimestamp(method)}
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+ `;
1018
+ }
1019
+
1020
+ /**
1021
+ * Get emoji icon based on payment method type
1022
+ * @param {string} type - Payment method type
1023
+ * @returns {string} Emoji icon
1024
+ */
1025
+ getPaymentMethodIcon(type) {
1026
+ const icons = {
1027
+ card: "💳",
1028
+ bankAccount: "🏦",
1029
+ wallet: "💰",
1030
+ applePay: "🍎",
1031
+ moovWallet: "💰",
1032
+ };
1033
+ return icons[type] || "💳";
1034
+ }
1035
+
1036
+ /**
1037
+ * Get SVG icon for timeline based on payment type
1038
+ * @param {string} type - Payment method type
1039
+ * @returns {string} SVG HTML string
1040
+ */
1041
+ getPaymentTypeIconSvg(type) {
1042
+ switch (type) {
1043
+ case "card":
1044
+ return `<svg viewBox="0 0 24 24" stroke-width="2"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>`;
1045
+ case "bankAccount":
1046
+ return `<svg viewBox="0 0 24 24" stroke-width="2"><path d="M3 21h18M3 10h18M5 6l7-3 7 3M4 10v11M20 10v11M8 14v3M12 14v3M16 14v3"></path></svg>`;
1047
+ case "wallet":
1048
+ case "moovWallet":
1049
+ return `<svg viewBox="0 0 24 24" stroke-width="2"><path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5z"></path><path d="M16 12h.01"></path></svg>`;
1050
+ case "applePay":
1051
+ return `<svg viewBox="0 0 24 24" stroke-width="2"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2z"></path><path d="M12 6v2M12 16v2M6 12h2M16 12h2"></path></svg>`;
1052
+ default:
1053
+ return `<svg viewBox="0 0 24 24" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Format payment method title based on type
1059
+ * @param {Object} method - Payment method data
1060
+ * @returns {string} Formatted title
1061
+ */
1062
+ formatPaymentMethodTitle(method) {
1063
+ switch (method.paymentMethodType) {
1064
+ case "card":
1065
+ if (method.card) {
1066
+ const brand = method.card.brand || method.card.cardType || "Card";
1067
+ return `${this.capitalizeFirst(brand)}`;
1068
+ }
1069
+ return "Credit/Debit Card";
1070
+ case "bankAccount":
1071
+ if (method.bankAccount) {
1072
+ return method.bankAccount.bankName || "Bank Account";
1073
+ }
1074
+ return "Bank Account";
1075
+ case "wallet":
1076
+ case "moovWallet":
1077
+ return "Moov Wallet";
1078
+ case "applePay":
1079
+ return "Apple Pay";
1080
+ default:
1081
+ return "Payment Method";
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * Format payment method description with details
1087
+ * @param {Object} method - Payment method data
1088
+ * @returns {string} Formatted description
1089
+ */
1090
+ formatPaymentMethodDescription(method) {
1091
+ switch (method.paymentMethodType) {
1092
+ case "card":
1093
+ if (method.card) {
1094
+ const lastFour = method.card.lastFourCardNumber || "****";
1095
+ const expiry = method.card.expiration
1096
+ ? `Expires ${method.card.expiration.month}/${method.card.expiration.year}`
1097
+ : "";
1098
+ return `Ending in ****${lastFour}${expiry ? ` • ${expiry}` : ""}`;
1099
+ }
1100
+ return "Card details unavailable";
1101
+ case "bankAccount":
1102
+ if (method.bankAccount) {
1103
+ const lastFour = method.bankAccount.lastFourAccountNumber || "****";
1104
+ const type = method.bankAccount.bankAccountType || "";
1105
+ return `${this.capitalizeFirst(
1106
+ type
1107
+ )} account ending in ****${lastFour}`;
1108
+ }
1109
+ return "Bank account details unavailable";
1110
+ case "wallet":
1111
+ case "moovWallet":
1112
+ if (method.wallet) {
1113
+ return `Available balance: $${(method.wallet.availableBalance?.value || 0) / 100
1114
+ }`;
1115
+ }
1116
+ return "Digital wallet for payments";
1117
+ case "applePay":
1118
+ if (method.applePay) {
1119
+ return `${method.applePay.brand || "Card"} via Apple Pay`;
1120
+ }
1121
+ return "Apple Pay enabled device";
1122
+ default:
1123
+ return "Payment method linked to your account";
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Format timestamp for payment method
1129
+ * @param {Object} method - Payment method data
1130
+ * @returns {string} Formatted timestamp
1131
+ */
1132
+ formatPaymentMethodTimestamp(method) {
1133
+ // Try to get creation date from various possible fields
1134
+ const dateStr = method.createdOn || method.createdAt || method.addedAt;
1135
+
1136
+ if (dateStr) {
1137
+ try {
1138
+ const date = new Date(dateStr);
1139
+ return `Added ${date.toLocaleDateString("en-US", {
1140
+ month: "short",
1141
+ day: "numeric",
1142
+ year: "numeric",
1143
+ })}`;
1144
+ } catch (e) {
1145
+ return "Date unavailable";
1146
+ }
1147
+ }
1148
+
1149
+ return "Recently added";
1150
+ }
1151
+
1152
+ /**
1153
+ * Capitalize first letter of string
1154
+ * @param {string} str - String to capitalize
1155
+ * @returns {string} Capitalized string
1156
+ */
1157
+ capitalizeFirst(str) {
1158
+ if (!str) return "";
1159
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1160
+ }
1161
+
1162
+ /**
1163
+ * Get SVG icon based on status (kept for backwards compatibility)
1164
+ * @param {string} status - Status type
1165
+ * @returns {string} SVG HTML string
1166
+ */
1167
+ getStatusIcon(status) {
1168
+ switch (status) {
1169
+ case "completed":
1170
+ return `<svg viewBox="0 0 24 24" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
1171
+ case "in-progress":
1172
+ return `<svg viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="3"></circle></svg>`;
1173
+ case "error":
1174
+ return `<svg viewBox="0 0 24 24" stroke-width="3"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
1175
+ case "pending":
1176
+ default:
1177
+ return `<svg viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="1"></circle></svg>`;
1178
+ }
1179
+ }
1180
+
1181
+ /**
1182
+ * Format status for display (kept for backwards compatibility)
1183
+ * @param {string} status - Status type
1184
+ * @returns {string} Formatted status text
1185
+ */
1186
+ formatStatus(status) {
1187
+ const statusMap = {
1188
+ completed: "Completed",
1189
+ "in-progress": "In Progress",
1190
+ pending: "Pending",
1191
+ error: "Error",
1192
+ };
1193
+ return statusMap[status] || status;
1194
+ }
1195
+
1196
+ /**
1197
+ * Render underwriting history as a timeline
1198
+ * @param {Array} history - Array of underwriting status records
1199
+ * @returns {string} HTML string for timeline
1200
+ */
1201
+ renderUnderwritingTimeline(history) {
1202
+ if (!history || history.length === 0) {
1203
+ return `
1204
+ <div style="display: flex; margin: 24px 0; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; background: var(--color-gray-50, #f9fafb); border: 1px dashed var(--color-gray-200, #d1d5db); border-radius: var(--radius-xl, 0.75rem);">
1205
+ <svg style="width: 64px; height: 64px; color: var(--color-gray-400, #9ca3af); margin-bottom: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
1206
+ <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
1207
+ </svg>
1208
+ <p style="font-size: 16px; color: var(--color-gray-500, #6b7280); margin: 0;">No underwriting history found</p>
1209
+ </div>
1210
+ `;
1211
+ }
1212
+
1213
+ // Sort by changedAt (oldest first, will be reversed in display)
1214
+ const sortedHistory = [...history].sort((a, b) => {
1215
+ const dateA = new Date(a.changedAt);
1216
+ const dateB = new Date(b.changedAt);
1217
+ return dateA - dateB;
1218
+ });
1219
+
1220
+ // Reverse to show newest at top
1221
+ const reversedHistory = [...sortedHistory].reverse();
1222
+
1223
+ const timelineItems = reversedHistory
1224
+ .map((item, index) => {
1225
+ const isNewest = index === 0;
1226
+ const isOldest = index === reversedHistory.length - 1;
1227
+ return this.renderUnderwritingTimelineItem(item, isNewest, isOldest);
1228
+ })
1229
+ .join("");
1230
+
1231
+ return `
1232
+ <style>
1233
+ .underwriting-timeline {
1234
+ padding: 16px;
1235
+ position: relative;
1236
+ }
1237
+
1238
+ .timeline-item {
1239
+ display: flex;
1240
+ gap: 12px;
1241
+ position: relative;
1242
+ padding-bottom: 24px;
1243
+ }
1244
+
1245
+ .timeline-item:last-child {
1246
+ padding-bottom: 0;
1247
+ }
1248
+
1249
+ .timeline-icon-wrapper {
1250
+ position: relative;
1251
+ display: flex;
1252
+ flex-direction: column;
1253
+ align-items: center;
1254
+ flex-shrink: 0;
1255
+ }
1256
+
1257
+ .timeline-icon {
1258
+ width: 32px;
1259
+ height: 32px;
1260
+ border-radius: 50%;
1261
+ display: flex;
1262
+ align-items: center;
1263
+ justify-content: center;
1264
+ z-index: 2;
1265
+ position: relative;
1266
+ background: var(--color-border, #e8e8e8);
1267
+ color: var(--color-gray-500, #6b7280);
1268
+ }
1269
+
1270
+ .timeline-icon.approved {
1271
+ background: var(--color-success-light, #d3f3df);
1272
+ color: var(--color-success, #22c55e);
1273
+ }
1274
+
1275
+ .timeline-icon.pending {
1276
+ background: var(--color-orange-100, #fdecce);
1277
+ color: var(--color-warning, #f59e0b);
1278
+ }
1279
+
1280
+ .timeline-icon.latest::before {
1281
+ content: '';
1282
+ position: absolute;
1283
+ width: 100%;
1284
+ height: 100%;
1285
+ border-radius: 50%;
1286
+ border: 2px solid currentColor;
1287
+ opacity: 0.6;
1288
+ animation: ping 1.2s cubic-bezier(0, 0, 0.2, 1) infinite;
1289
+ }
1290
+
1291
+ @keyframes ping {
1292
+ 0% {
1293
+ transform: scale(1);
1294
+ opacity: 0.6;
1295
+ }
1296
+ 75%, 100% {
1297
+ transform: scale(1.4);
1298
+ opacity: 0;
1299
+ }
1300
+ }
1301
+
1302
+ .timeline-line {
1303
+ position: absolute;
1304
+ top: 32px;
1305
+ left: 50%;
1306
+ transform: translateX(-50%);
1307
+ width: 2px;
1308
+ height: calc(100% - 0px);
1309
+ background: var(--color-border, #e8e8e8);
1310
+ z-index: 1;
1311
+ }
1312
+
1313
+ .timeline-content {
1314
+ flex: 1;
1315
+ background: var(--color-white, #fff);
1316
+ border: 1px solid var(--color-border, #e8e8e8);
1317
+ border-radius: var(--radius-lg, 0.5rem);
1318
+ padding: 12px 16px;
1319
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
1320
+ }
1321
+
1322
+ .timeline-status {
1323
+ font-size: 13px;
1324
+ font-weight: 600;
1325
+ color: var(--color-headline, #0f2a39);
1326
+ margin: 0 0 4px 0;
1327
+ }
1328
+
1329
+ .timeline-date {
1330
+ font-size: 12px;
1331
+ color: var(--color-gray-400, #9ca3af);
1332
+ margin: 0;
1333
+ }
1334
+
1335
+ .timeline-badge {
1336
+ display: inline-block;
1337
+ padding: 2px 8px;
1338
+ border-radius: 10px;
1339
+ font-size: 10px;
1340
+ font-weight: 500;
1341
+ margin-top: 6px;
1342
+ }
1343
+
1344
+ .timeline-badge.newest {
1345
+ background: var(--color-primary-light, #e8f0eb);
1346
+ color: var(--color-primary, #4c7b63);
1347
+ }
1348
+
1349
+ .timeline-badge.oldest {
1350
+ background: var(--color-gray-100, #f3f4f6);
1351
+ color: var(--color-gray-500, #6b7280);
1352
+ }
1353
+ </style>
1354
+
1355
+ <div class="underwriting-timeline">
1356
+ ${timelineItems}
1357
+ </div>
1358
+ `;
1359
+ }
1360
+
1361
+ /**
1362
+ * Render a single underwriting timeline item
1363
+ * @param {Object} item - Underwriting status record
1364
+ * @param {boolean} isNewest - Whether this is the newest status
1365
+ * @param {boolean} isOldest - Whether this is the oldest status
1366
+ * @returns {string} HTML string for timeline item
1367
+ */
1368
+ renderUnderwritingTimelineItem(item, isNewest, isOldest) {
1369
+ const status = item.status || "unknown";
1370
+ const statusDisplay = this.formatUnderwritingStatus(status);
1371
+ const icon = this.getUnderwritingStatusIcon(status);
1372
+ const formattedDate = this.formatUnderwritingDate(item.changedAt);
1373
+
1374
+ return `
1375
+ <div class="timeline-item">
1376
+ <div class="timeline-icon-wrapper">
1377
+ <div class="timeline-icon ${status} ${isNewest ? "latest" : ""}">
1378
+ ${icon}
1379
+ </div>
1380
+ ${!isOldest ? '<div class="timeline-line"></div>' : ""}
1381
+ </div>
1382
+ <div class="timeline-content">
1383
+ <h4 class="timeline-status">${statusDisplay}</h4>
1384
+ <p class="timeline-date">${formattedDate}</p>
1385
+ ${isNewest ? '<span class="timeline-badge newest">Latest</span>' : ""}
1386
+ ${isOldest ? '<span class="timeline-badge oldest">Initial</span>' : ""
1387
+ }
1388
+ </div>
1389
+ </div>
1390
+ `;
1391
+ }
1392
+
1393
+ /**
1394
+ * Format underwriting status for display
1395
+ * @param {string} status - Status value
1396
+ * @returns {string} Formatted status text
1397
+ */
1398
+ formatUnderwritingStatus(status) {
1399
+ const statusMap = {
1400
+ pending: "Pending Review",
1401
+ approved: "Approved",
1402
+ rejected: "Rejected",
1403
+ under_review: "Under Review",
1404
+ submitted: "Submitted",
1405
+ };
1406
+ return (
1407
+ statusMap[status] || status.charAt(0).toUpperCase() + status.slice(1)
1408
+ );
1409
+ }
1410
+
1411
+ /**
1412
+ * Get icon SVG for underwriting status
1413
+ * @param {string} status - Status value
1414
+ * @returns {string} SVG HTML string
1415
+ */
1416
+ getUnderwritingStatusIcon(status) {
1417
+ switch (status) {
1418
+ case "approved":
1419
+ return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
1420
+ case "rejected":
1421
+ return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
1422
+ case "under_review":
1423
+ return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
1424
+ case "pending":
1425
+ default:
1426
+ return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`;
1427
+ }
1428
+ }
1429
+
1430
+ /**
1431
+ * Format date for underwriting timeline
1432
+ * @param {string} dateStr - ISO date string
1433
+ * @returns {string} Formatted date string
1434
+ */
1435
+ formatUnderwritingDate(dateStr) {
1436
+ if (!dateStr) return "Date unavailable";
1437
+
1438
+ try {
1439
+ const date = new Date(dateStr);
1440
+ const dateFormatted = date.toLocaleDateString("en-US", {
1441
+ month: "short",
1442
+ day: "numeric",
1443
+ year: "numeric",
1444
+ });
1445
+ const timeFormatted = date.toLocaleTimeString("en-US", {
1446
+ hour: "numeric",
1447
+ minute: "2-digit",
1448
+ hour12: true,
1449
+ });
1450
+ return `${dateFormatted} ${timeFormatted}`;
1451
+ } catch (e) {
1452
+ return dateStr;
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ // Register the custom element only if it hasn't been registered yet
1458
+ if (!customElements.get("operator-underwriting")) {
1459
+ customElements.define("operator-underwriting", OperatorUnderwriting);
1460
+ }
1461
+
1462
+ // Export for module usage
1463
+ if (typeof module !== "undefined" && module.exports) {
1464
+ module.exports = { OperatorUnderwriting };
1465
+ }
1466
+
1467
+ // Make available globally for script tag usage
1468
+ if (typeof window !== "undefined") {
1469
+ window.OperatorUnderwriting = OperatorUnderwriting;
1470
+ }
1471
+
1472
+ // Export for ES6 modules
1473
+ export { OperatorUnderwriting };