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,3750 @@
1
+ /**
2
+ * Operator Onboarding Web Component
3
+ *
4
+ * A web component that captures operator information via stepper form
5
+ * with necessary field validations. This serves as the simplified approach
6
+ * in comparison to the Moov Onboarding Drop.
7
+ *
8
+ * @requires BisonJibPayAPI - Must be loaded before this component (from api.js)
9
+ *
10
+ * @author @kfajardo
11
+ * @version 1.0.0
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <script src="api.js"></script>
16
+ * <script src="operator-onboarding.js"></script>
17
+ *
18
+ * <operator-onboarding id="onboarding"></operator-onboarding>
19
+ * <script>
20
+ * const onboarding = document.getElementById('onboarding');
21
+ * onboarding.onSuccess = (data) => console.log('Success!', data);
22
+ * onboarding.onError = (error) => console.error('Error:', error);
23
+ * </script>
24
+ * ```
25
+ */
26
+
27
+ class OperatorOnboarding extends HTMLElement {
28
+ constructor() {
29
+ super();
30
+ this.attachShadow({ mode: "open" });
31
+
32
+ // API Configuration
33
+ this.apiBaseURL =
34
+ this.getAttribute("api-base-url") ||
35
+ "https://bison-jib-development.azurewebsites.net";
36
+ this.embeddableKey =
37
+ this.getAttribute("embeddable-key") ||
38
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
39
+
40
+ // Check if BisonJibPayAPI is available
41
+ if (typeof BisonJibPayAPI === "undefined") {
42
+ console.error(
43
+ "OperatorOnboarding: BisonJibPayAPI is not available. Please ensure api.js is loaded before operator-onboarding.js"
44
+ );
45
+ this.api = null;
46
+ } else {
47
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
48
+ }
49
+
50
+ // Initialize state
51
+ this.state = {
52
+ isModalOpen: false,
53
+ currentStep: 0,
54
+ totalSteps: 4, // Business, Representatives, Bank, Underwriting
55
+ isSubmitted: false,
56
+ isFailed: false,
57
+ isSubmissionFailed: false,
58
+ formData: {
59
+ businessDetails: {
60
+ businessName: "",
61
+ doingBusinessAs: "",
62
+ ein: "",
63
+ businessWebsite: "",
64
+ businessPhoneNumber: "",
65
+ businessEmail: "",
66
+ BusinessAddress1: "",
67
+ businessCity: "",
68
+ businessState: "",
69
+ businessPostalCode: "",
70
+ },
71
+ representatives: [],
72
+ underwriting: {
73
+ underwritingDocuments: [], // File upload support for underwriting documents
74
+ },
75
+ bankDetails: {
76
+ bankAccountHolderName: "",
77
+ bankAccountType: "checking",
78
+ bankRoutingNumber: "",
79
+ bankAccountNumber: "",
80
+ },
81
+ },
82
+ validationState: {
83
+ step0: { isValid: false, errors: {} }, // Business Details
84
+ step1: { isValid: true, errors: {} }, // Representatives (optional)
85
+ step2: { isValid: false, errors: {} }, // Bank Details
86
+ step3: { isValid: false, errors: {} }, // Underwriting (required)
87
+ },
88
+ completedSteps: new Set(),
89
+ uiState: {
90
+ isLoading: false,
91
+ showErrors: false,
92
+ errorMessage: null,
93
+ },
94
+ };
95
+
96
+ // Step configuration (Verification is now pre-stepper)
97
+ this.STEPS = [
98
+ {
99
+ id: "business-details",
100
+ title: "Business",
101
+ description: "Provide your business details",
102
+ canSkip: false,
103
+ },
104
+ {
105
+ id: "representatives",
106
+ title: "Representatives",
107
+ description: "Add business representatives (optional)",
108
+ canSkip: true,
109
+ },
110
+ {
111
+ id: "bank-details",
112
+ title: "Bank Account",
113
+ description: "Link your bank account",
114
+ canSkip: false,
115
+ },
116
+ {
117
+ id: "underwriting",
118
+ title: "Underwriting",
119
+ description: "Upload required documents",
120
+ canSkip: false,
121
+ },
122
+ ];
123
+
124
+ // US States for dropdown
125
+ this.US_STATES = [
126
+ "AL",
127
+ "AK",
128
+ "AZ",
129
+ "AR",
130
+ "CA",
131
+ "CO",
132
+ "CT",
133
+ "DE",
134
+ "FL",
135
+ "GA",
136
+ "HI",
137
+ "ID",
138
+ "IL",
139
+ "IN",
140
+ "IA",
141
+ "KS",
142
+ "KY",
143
+ "LA",
144
+ "ME",
145
+ "MD",
146
+ "MA",
147
+ "MI",
148
+ "MN",
149
+ "MS",
150
+ "MO",
151
+ "MT",
152
+ "NE",
153
+ "NV",
154
+ "NH",
155
+ "NJ",
156
+ "NM",
157
+ "NY",
158
+ "NC",
159
+ "ND",
160
+ "OH",
161
+ "OK",
162
+ "OR",
163
+ "PA",
164
+ "RI",
165
+ "SC",
166
+ "SD",
167
+ "TN",
168
+ "TX",
169
+ "UT",
170
+ "VT",
171
+ "VA",
172
+ "WA",
173
+ "WV",
174
+ "WI",
175
+ "WY",
176
+ ];
177
+
178
+ // Internal callback storage
179
+ this._onSuccessCallback = null;
180
+ this._onErrorCallback = null;
181
+ this._onSubmitCallback = null;
182
+ this._onConfirmCallback = null;
183
+ this._initialData = null;
184
+
185
+ this.render();
186
+ }
187
+
188
+ // Getter and setter for onSuccess property (for easy framework integration)
189
+ get onSuccess() {
190
+ return this._onSuccessCallback;
191
+ }
192
+
193
+ set onSuccess(callback) {
194
+ console.log("OperatorOnboarding: onSuccess setter called", {
195
+ callbackType: typeof callback,
196
+ isFunction: typeof callback === "function",
197
+ });
198
+ if (typeof callback === "function" || callback === null) {
199
+ this._onSuccessCallback = callback;
200
+ }
201
+ }
202
+
203
+ // Getter and setter for onError property (for error handling)
204
+ get onError() {
205
+ return this._onErrorCallback;
206
+ }
207
+
208
+ set onError(callback) {
209
+ if (typeof callback === "function" || callback === null) {
210
+ this._onErrorCallback = callback;
211
+ }
212
+ }
213
+
214
+ // Getter and setter for onSubmit property (for pre-submission handling)
215
+ get onSubmit() {
216
+ return this._onSubmitCallback;
217
+ }
218
+
219
+ set onSubmit(callback) {
220
+ if (typeof callback === "function" || callback === null) {
221
+ this._onSubmitCallback = callback;
222
+ }
223
+ }
224
+
225
+ // Getter and setter for onConfirm property (for success confirmation button)
226
+ get onConfirm() {
227
+ return this._onConfirmCallback;
228
+ }
229
+
230
+ set onConfirm(callback) {
231
+ if (typeof callback === "function" || callback === null) {
232
+ this._onConfirmCallback = callback;
233
+ }
234
+ }
235
+
236
+ // Getter and setter for onLoad property (for pre-populating form data)
237
+ get onLoad() {
238
+ return this._initialData;
239
+ }
240
+
241
+ set onLoad(data) {
242
+ if (data && typeof data === "object") {
243
+ this._initialData = data;
244
+ this.loadInitialData(data);
245
+ }
246
+ }
247
+
248
+ // Static getter for observed attributes
249
+ static get observedAttributes() {
250
+ return ["on-success", "on-error", "on-submit", "on-load"];
251
+ }
252
+
253
+ // ==================== VALIDATORS ====================
254
+
255
+ validators = {
256
+ required: (value, fieldName) => ({
257
+ isValid: value && value.trim().length > 0,
258
+ error: `${fieldName} is required`,
259
+ }),
260
+
261
+ email: (value) => ({
262
+ isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
263
+ error: "Please enter a valid email address",
264
+ }),
265
+
266
+ usPhone: (value) => {
267
+ const cleaned = value.replace(/\D/g, "");
268
+ return {
269
+ isValid: cleaned.length === 10,
270
+ error: "Please enter a valid 10-digit U.S. phone number",
271
+ };
272
+ },
273
+
274
+ routingNumber: (value) => {
275
+ const cleaned = value.replace(/\D/g, "");
276
+ return {
277
+ isValid: cleaned.length === 9,
278
+ error: "Routing number must be 9 digits",
279
+ };
280
+ },
281
+
282
+ accountNumber: (value) => {
283
+ const cleaned = value.replace(/\D/g, "");
284
+ return {
285
+ isValid: cleaned.length >= 4 && cleaned.length <= 17,
286
+ error: "Account number must be 4-17 digits",
287
+ };
288
+ },
289
+
290
+ ein: (value) => {
291
+ const cleaned = value.replace(/\D/g, "");
292
+ return {
293
+ isValid: cleaned.length === 9,
294
+ error: "EIN must be 9 digits",
295
+ };
296
+ },
297
+
298
+ url: (value) => {
299
+ if (!value) return { isValid: true, error: "" }; // Optional
300
+
301
+ // Trim whitespace
302
+ const trimmed = value.trim();
303
+ if (!trimmed) return { isValid: true, error: "" };
304
+
305
+ // Pattern for basic domain validation
306
+ // Accepts: domain.com, www.domain.com, subdomain.domain.com
307
+ const domainPattern = /^(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$/;
308
+
309
+ // Check if it's already a full URL
310
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
311
+ try {
312
+ new URL(trimmed);
313
+ return { isValid: true, error: "", normalizedValue: trimmed };
314
+ } catch {
315
+ return { isValid: false, error: "Please enter a valid URL" };
316
+ }
317
+ }
318
+
319
+ // Check if it matches domain pattern (without protocol)
320
+ if (domainPattern.test(trimmed)) {
321
+ // Auto-normalize by adding https://
322
+ const normalized = `https://${trimmed}`;
323
+ try {
324
+ new URL(normalized); // Validate the normalized URL
325
+ return { isValid: true, error: "", normalizedValue: normalized };
326
+ } catch {
327
+ return { isValid: false, error: "Please enter a valid URL" };
328
+ }
329
+ }
330
+
331
+ return {
332
+ isValid: false,
333
+ error:
334
+ "Please enter a valid URL (e.g., example.com, www.example.com, or https://example.com)",
335
+ };
336
+ },
337
+
338
+ postalCode: (value) => {
339
+ const cleaned = value.replace(/\D/g, "");
340
+ return {
341
+ isValid: cleaned.length === 5,
342
+ error: "Please enter a valid 5-digit ZIP code",
343
+ };
344
+ },
345
+ };
346
+
347
+ // ==================== STATE MANAGEMENT ====================
348
+
349
+ setState(newState) {
350
+ const wasModalOpen = this.state.isModalOpen;
351
+
352
+ this.state = {
353
+ ...this.state,
354
+ ...newState,
355
+ formData: {
356
+ ...this.state.formData,
357
+ ...(newState.formData || {}),
358
+ },
359
+ validationState: {
360
+ ...this.state.validationState,
361
+ ...(newState.validationState || {}),
362
+ },
363
+ uiState: {
364
+ ...this.state.uiState,
365
+ ...(newState.uiState || {}),
366
+ },
367
+ };
368
+
369
+ // Track if modal was already open to skip animations on content updates
370
+ this._skipModalAnimation = wasModalOpen && this.state.isModalOpen;
371
+ this.render();
372
+ }
373
+
374
+ // ==================== VALIDATION ====================
375
+
376
+ validateStep(stepIdentifier) {
377
+ // Handle both step index (number) and step id (string)
378
+ let step;
379
+ let stepKey;
380
+
381
+ if (typeof stepIdentifier === "number") {
382
+ step = this.STEPS[stepIdentifier];
383
+ stepKey = `step${stepIdentifier}`;
384
+ } else {
385
+ step = this.STEPS.find((s) => s.id === stepIdentifier);
386
+ stepKey = stepIdentifier;
387
+ }
388
+
389
+ if (!step) return false;
390
+
391
+ let isValid = true;
392
+ const errors = {};
393
+
394
+ // Update validation state
395
+ this.setState({
396
+ validationState: {
397
+ [stepKey]: { isValid, errors },
398
+ },
399
+ uiState: { showErrors: !isValid },
400
+ });
401
+
402
+ return isValid;
403
+ }
404
+
405
+ validateField(value, validators, fieldName) {
406
+ for (const validatorName of validators) {
407
+ const validator = this.validators[validatorName];
408
+ if (validator) {
409
+ const result = validator(value, fieldName);
410
+ if (!result.isValid) {
411
+ return result.error;
412
+ }
413
+ }
414
+ }
415
+ return "";
416
+ }
417
+
418
+ validateCurrentStep() {
419
+ const stepId = this.STEPS[this.state.currentStep].id;
420
+ let isValid = true;
421
+ const errors = {};
422
+
423
+ if (stepId === "business-details") {
424
+ const data = this.state.formData.businessDetails;
425
+ const fields = [
426
+ {
427
+ name: "businessName",
428
+ validators: ["required"],
429
+ label: "Business Name",
430
+ },
431
+ {
432
+ name: "doingBusinessAs",
433
+ validators: ["required"],
434
+ label: "Doing Business As (DBA)",
435
+ },
436
+ {
437
+ name: "ein",
438
+ validators: ["required", "ein"],
439
+ label: "EIN",
440
+ },
441
+ {
442
+ name: "businessWebsite",
443
+ validators: ["required", "url"],
444
+ label: "Business Website",
445
+ },
446
+ {
447
+ name: "businessPhoneNumber",
448
+ validators: ["required", "usPhone"],
449
+ label: "Business Phone",
450
+ },
451
+ {
452
+ name: "businessEmail",
453
+ validators: ["required", "email"],
454
+ label: "Business Email",
455
+ },
456
+ {
457
+ name: "BusinessAddress1",
458
+ validators: ["required"],
459
+ label: "Street Address",
460
+ },
461
+ { name: "businessCity", validators: ["required"], label: "City" },
462
+ { name: "businessState", validators: ["required"], label: "State" },
463
+ {
464
+ name: "businessPostalCode",
465
+ validators: ["required", "postalCode"],
466
+ label: "ZIP Code",
467
+ },
468
+ ];
469
+
470
+ fields.forEach((field) => {
471
+ const error = this.validateField(
472
+ data[field.name],
473
+ field.validators,
474
+ field.label
475
+ );
476
+ if (error) {
477
+ errors[field.name] = error;
478
+ isValid = false;
479
+ }
480
+ });
481
+ } else if (stepId === "representatives") {
482
+ // Validate each representative if any field is filled
483
+ this.state.formData.representatives.forEach((rep, index) => {
484
+ const hasAnyValue = Object.values(rep).some(
485
+ (v) =>
486
+ (typeof v === "string" && v.trim()) ||
487
+ (typeof v === "object" &&
488
+ Object.values(v).some((av) => av && av.trim()))
489
+ );
490
+
491
+ if (hasAnyValue) {
492
+ const requiredFields = [
493
+ {
494
+ name: "representativeFirstName",
495
+ validators: ["required"],
496
+ label: "First Name",
497
+ },
498
+ {
499
+ name: "representativeLastName",
500
+ validators: ["required"],
501
+ label: "Last Name",
502
+ },
503
+ {
504
+ name: "representativeJobTitle",
505
+ validators: ["required"],
506
+ label: "Job Title",
507
+ },
508
+ {
509
+ name: "representativePhone",
510
+ validators: ["required", "usPhone"],
511
+ label: "Phone",
512
+ },
513
+ {
514
+ name: "representativeEmail",
515
+ validators: ["required", "email"],
516
+ label: "Email",
517
+ },
518
+ {
519
+ name: "representativeDateOfBirth",
520
+ validators: ["required"],
521
+ label: "Date of Birth",
522
+ },
523
+ {
524
+ name: "representativeAddress",
525
+ validators: ["required"],
526
+ label: "Address",
527
+ },
528
+ {
529
+ name: "representativeCity",
530
+ validators: ["required"],
531
+ label: "City",
532
+ },
533
+ {
534
+ name: "representativeState",
535
+ validators: ["required"],
536
+ label: "State",
537
+ },
538
+ {
539
+ name: "representativeZip",
540
+ validators: ["required", "postalCode"],
541
+ label: "ZIP Code",
542
+ },
543
+ ];
544
+
545
+ requiredFields.forEach((field) => {
546
+ const error = this.validateField(
547
+ rep[field.name],
548
+ field.validators,
549
+ field.label
550
+ );
551
+ if (error) {
552
+ if (!errors[`rep${index}`]) errors[`rep${index}`] = {};
553
+ errors[`rep${index}`][field.name] = error;
554
+ isValid = false;
555
+ }
556
+ });
557
+ }
558
+ });
559
+ } else if (stepId === "underwriting") {
560
+ // Validate that at least one document is uploaded
561
+ const data = this.state.formData.underwriting;
562
+ if (
563
+ !data.underwritingDocuments ||
564
+ data.underwritingDocuments.length === 0
565
+ ) {
566
+ errors.underwritingDocuments = "At least one document is required";
567
+ isValid = false;
568
+ }
569
+ } else if (stepId === "bank-details") {
570
+ const data = this.state.formData.bankDetails;
571
+ const fields = [
572
+ {
573
+ name: "bankAccountHolderName",
574
+ validators: ["required"],
575
+ label: "Account Holder Name",
576
+ },
577
+ {
578
+ name: "bankAccountType",
579
+ validators: ["required"],
580
+ label: "Account Type",
581
+ },
582
+ {
583
+ name: "bankRoutingNumber",
584
+ validators: ["required", "bankRoutingNumber"],
585
+ label: "Routing Number",
586
+ },
587
+ {
588
+ name: "bankAccountNumber",
589
+ validators: ["required", "bankAccountNumber"],
590
+ label: "Account Number",
591
+ },
592
+ ];
593
+
594
+ fields.forEach((field) => {
595
+ const error = this.validateField(
596
+ data[field.name],
597
+ field.validators,
598
+ field.label
599
+ );
600
+ if (error) {
601
+ errors[field.name] = error;
602
+ isValid = false;
603
+ }
604
+ });
605
+ }
606
+
607
+ this.setState({
608
+ validationState: {
609
+ [`step${this.state.currentStep}`]: { isValid, errors },
610
+ },
611
+ uiState: { showErrors: !isValid },
612
+ });
613
+
614
+ return isValid;
615
+ }
616
+
617
+ // ==================== NAVIGATION ====================
618
+
619
+ async goToNextStep() {
620
+ // Validate current step
621
+ const isValid = this.validateCurrentStep();
622
+
623
+ console.log("🔍 Validation Result:", {
624
+ currentStep: this.state.currentStep,
625
+ stepId: this.STEPS[this.state.currentStep].id,
626
+ isValid,
627
+ errors:
628
+ this.state.validationState[`step${this.state.currentStep}`]?.errors,
629
+ });
630
+
631
+ if (!isValid) {
632
+ console.warn("❌ Validation failed - cannot proceed to next step");
633
+ return;
634
+ }
635
+
636
+ // Mark step complete
637
+ const completedSteps = new Set(this.state.completedSteps);
638
+ completedSteps.add(this.state.currentStep);
639
+
640
+ // Progress to next step
641
+ if (this.state.currentStep < this.state.totalSteps - 1) {
642
+ console.log("✅ Moving to next step:", this.state.currentStep + 1);
643
+ this.setState({
644
+ currentStep: this.state.currentStep + 1,
645
+ completedSteps,
646
+ uiState: { showErrors: false },
647
+ });
648
+ } else {
649
+ console.log("✅ Final step - submitting form");
650
+ this.handleFormCompletion();
651
+ }
652
+ }
653
+
654
+ goToPreviousStep() {
655
+ if (this.state.currentStep > 0) {
656
+ this.setState({
657
+ currentStep: this.state.currentStep - 1,
658
+ uiState: { showErrors: false },
659
+ });
660
+ }
661
+ }
662
+
663
+ goToStep(stepIndex) {
664
+ if (
665
+ this.state.completedSteps.has(stepIndex) ||
666
+ stepIndex < this.state.currentStep
667
+ ) {
668
+ this.setState({
669
+ currentStep: stepIndex,
670
+ uiState: { showErrors: false },
671
+ });
672
+ }
673
+ }
674
+
675
+ skipStep() {
676
+ if (this.STEPS[this.state.currentStep].canSkip) {
677
+ const completedSteps = new Set(this.state.completedSteps);
678
+ completedSteps.add(this.state.currentStep);
679
+
680
+ this.setState({
681
+ currentStep: this.state.currentStep + 1,
682
+ completedSteps,
683
+ uiState: { showErrors: false },
684
+ });
685
+ }
686
+ }
687
+
688
+ // ==================== REPRESENTATIVES CRUD ====================
689
+
690
+ addRepresentative() {
691
+ const newRep = {
692
+ id: crypto.randomUUID(),
693
+ representativeFirstName: "",
694
+ representativeLastName: "",
695
+ representativeJobTitle: "",
696
+ representativePhone: "",
697
+ representativeEmail: "",
698
+ representativeDateOfBirth: "",
699
+ representativeAddress: "",
700
+ representativeCity: "",
701
+ representativeState: "",
702
+ representativeZip: "",
703
+ };
704
+
705
+ this.setState({
706
+ formData: {
707
+ representatives: [...this.state.formData.representatives, newRep],
708
+ },
709
+ });
710
+ }
711
+
712
+ removeRepresentative(index) {
713
+ const representatives = this.state.formData.representatives.filter(
714
+ (_, i) => i !== index
715
+ );
716
+ this.setState({
717
+ formData: { representatives },
718
+ });
719
+ }
720
+
721
+ updateRepresentative(index, field, value) {
722
+ const representatives = [...this.state.formData.representatives];
723
+ representatives[index] = {
724
+ ...representatives[index],
725
+ [field]: value,
726
+ };
727
+
728
+ this.setState({
729
+ formData: { representatives },
730
+ });
731
+ }
732
+
733
+ // ==================== INITIAL DATA LOADING ====================
734
+
735
+ loadInitialData(data) {
736
+ const newFormData = { ...this.state.formData };
737
+
738
+ // Load business details
739
+ if (data.businessDetails) {
740
+ newFormData.businessDetails = {
741
+ ...newFormData.businessDetails,
742
+ ...data.businessDetails,
743
+ };
744
+ }
745
+
746
+ // Load representatives
747
+ if (data.representatives && Array.isArray(data.representatives)) {
748
+ newFormData.representatives = data.representatives.map((rep) => ({
749
+ id: rep.id || crypto.randomUUID(),
750
+ representativeFirstName: rep.representativeFirstName || "",
751
+ representativeLastName: rep.representativeLastName || "",
752
+ representativeJobTitle: rep.representativeJobTitle || "",
753
+ representativePhone: rep.representativePhone || "",
754
+ representativeEmail: rep.representativeEmail || "",
755
+ representativeDateOfBirth: rep.representativeDateOfBirth || "",
756
+ representativeAddress: rep.representativeAddress || "",
757
+ representativeCity: rep.representativeCity || "",
758
+ representativeState: rep.representativeState || "",
759
+ representativeZip: rep.representativeZip || "",
760
+ }));
761
+ }
762
+
763
+ // Load underwriting
764
+ if (data.underwriting) {
765
+ newFormData.underwriting = {
766
+ ...newFormData.underwriting,
767
+ ...data.underwriting,
768
+ };
769
+ }
770
+
771
+ // Load bank details
772
+ if (data.bankDetails) {
773
+ newFormData.bankDetails = {
774
+ ...newFormData.bankDetails,
775
+ ...data.bankDetails,
776
+ };
777
+ }
778
+
779
+ // Update state with loaded data and initial step if provided
780
+ const newState = {
781
+ formData: newFormData,
782
+ };
783
+
784
+ // Set initial step if provided (0-indexed)
785
+ if (typeof data.initialStep === "number" && data.initialStep >= 0 && data.initialStep < this.state.totalSteps) {
786
+ newState.currentStep = data.initialStep;
787
+ }
788
+
789
+ this.setState(newState);
790
+ }
791
+
792
+ /**
793
+ * Reset form to initial state or to onLoad values if provided
794
+ */
795
+ resetForm() {
796
+ // Default empty form data
797
+ const defaultFormData = {
798
+ businessDetails: {
799
+ businessName: "",
800
+ doingBusinessAs: "",
801
+ ein: "",
802
+ businessWebsite: "",
803
+ businessPhoneNumber: "",
804
+ businessEmail: "",
805
+ BusinessAddress1: "",
806
+ businessCity: "",
807
+ businessState: "",
808
+ businessPostalCode: "",
809
+ },
810
+ representatives: [],
811
+ underwriting: {
812
+ underwritingDocuments: [],
813
+ },
814
+ bankDetails: {
815
+ bankAccountHolderName: "",
816
+ bankAccountType: "checking",
817
+ bankRoutingNumber: "",
818
+ bankAccountNumber: "",
819
+ },
820
+ };
821
+
822
+ // Default validation state
823
+ const defaultValidationState = {
824
+ step0: { isValid: false, errors: {} },
825
+ step1: { isValid: true, errors: {} },
826
+ step2: { isValid: false, errors: {} },
827
+ step3: { isValid: false, errors: {} },
828
+ };
829
+
830
+ // Reset to defaults
831
+ this.state = {
832
+ ...this.state,
833
+ currentStep: 0,
834
+ isSubmitted: false,
835
+ isFailed: false,
836
+ isSubmissionFailed: false,
837
+ formData: defaultFormData,
838
+ validationState: defaultValidationState,
839
+ completedSteps: new Set(),
840
+ uiState: {
841
+ isLoading: false,
842
+ showErrors: false,
843
+ errorMessage: null,
844
+ },
845
+ };
846
+
847
+ // If we have initial data from onLoad, re-apply it
848
+ if (this._initialData) {
849
+ this.loadInitialData(this._initialData);
850
+ }
851
+ }
852
+
853
+ // ==================== UTILITIES ====================
854
+
855
+ formatPhoneNumber(value) {
856
+ // Remove all non-digits
857
+ const cleaned = value.replace(/\D/g, "");
858
+
859
+ // Limit to 10 digits
860
+ const limited = cleaned.slice(0, 10);
861
+
862
+ // Format progressively as (XXX) XXX-XXXX
863
+ if (limited.length === 0) {
864
+ return "";
865
+ } else if (limited.length <= 3) {
866
+ return limited;
867
+ } else if (limited.length <= 6) {
868
+ return `(${limited.slice(0, 3)}) ${limited.slice(3)}`;
869
+ } else {
870
+ return `(${limited.slice(0, 3)}) ${limited.slice(3, 6)}-${limited.slice(
871
+ 6
872
+ )}`;
873
+ }
874
+ }
875
+
876
+ formatEIN(value) {
877
+ // Remove all non-digits
878
+ const cleaned = value.replace(/\D/g, "");
879
+
880
+ // Limit to 9 digits
881
+ const limited = cleaned.slice(0, 9);
882
+
883
+ // Format as XX-XXXXXXX
884
+ if (limited.length <= 2) {
885
+ return limited;
886
+ } else {
887
+ return `${limited.slice(0, 2)}-${limited.slice(2)}`;
888
+ }
889
+ }
890
+
891
+ getFieldError(fieldName, repIndex = null) {
892
+ if (!this.state.uiState.showErrors) return "";
893
+
894
+ // For stepper steps
895
+ const errors =
896
+ this.state.validationState[`step${this.state.currentStep}`]?.errors || {};
897
+
898
+ if (repIndex !== null) {
899
+ return errors[`rep${repIndex}`]?.[fieldName] || "";
900
+ }
901
+
902
+ return errors[fieldName] || "";
903
+ }
904
+
905
+ // ==================== FORM COMPLETION ====================
906
+
907
+ async handleFormCompletion(shouldFail = false) {
908
+ console.log("OperatorOnboarding: handleFormCompletion STARTED");
909
+
910
+ const completedSteps = new Set(this.state.completedSteps);
911
+ completedSteps.add(this.state.currentStep);
912
+
913
+ // Prepare form data
914
+ const formData = {
915
+ businessDetails: this.state.formData.businessDetails,
916
+ representatives: this.state.formData.representatives,
917
+ underwriting: this.state.formData.underwriting,
918
+ bankDetails: this.state.formData.bankDetails,
919
+ };
920
+
921
+ // Call onSubmit callback if provided (before submission)
922
+ let processedData = formData;
923
+ if (this.onSubmit && typeof this.onSubmit === "function") {
924
+ try {
925
+ const result = await this.onSubmit(formData);
926
+
927
+ // If callback returns false, cancel submission
928
+ if (result === false) {
929
+ console.log("Form submission cancelled by onSubmit callback");
930
+ return;
931
+ }
932
+
933
+ // If callback returns modified data, use it
934
+ if (result && typeof result === "object") {
935
+ processedData = result;
936
+ }
937
+ } catch (error) {
938
+ console.error("Error in onSubmit callback:", error);
939
+ this.handleSubmissionFailure(formData);
940
+ return;
941
+ }
942
+ }
943
+
944
+ // Show loading state
945
+ this.setState({
946
+ completedSteps,
947
+ uiState: { isLoading: true },
948
+ });
949
+
950
+ console.log("Form Submission - Complete Data:", processedData);
951
+
952
+ // Check if API is available
953
+ if (!this.api) {
954
+ console.error("OperatorOnboarding: API not available for registration");
955
+ this.handleSubmissionFailure(processedData);
956
+ return;
957
+ }
958
+
959
+ try {
960
+ // Build FormData for API submission
961
+ const apiFormData = new FormData();
962
+
963
+ // Add business details
964
+ const businessDetails = processedData.businessDetails;
965
+ apiFormData.append("businessName", businessDetails.businessName || "");
966
+ apiFormData.append("doingBusinessAs", businessDetails.doingBusinessAs || "");
967
+ apiFormData.append("ein", businessDetails.ein || "");
968
+ apiFormData.append("businessWebsite", businessDetails.businessWebsite || "");
969
+ apiFormData.append("businessPhoneNumber", businessDetails.businessPhoneNumber || "");
970
+ apiFormData.append("businessEmail", businessDetails.businessEmail || "");
971
+ apiFormData.append("businessAddress1", businessDetails.BusinessAddress1 || "");
972
+ apiFormData.append("businessCity", businessDetails.businessCity || "");
973
+ apiFormData.append("businessState", businessDetails.businessState || "");
974
+ apiFormData.append("businessPostalCode", businessDetails.businessPostalCode || "");
975
+
976
+ // Add representatives as JSON string
977
+ if (processedData.representatives && processedData.representatives.length > 0) {
978
+ apiFormData.append("representatives", JSON.stringify(processedData.representatives));
979
+ }
980
+
981
+ // Add bank details
982
+ const bankDetails = processedData.bankDetails;
983
+ apiFormData.append("bankAccountHolderName", bankDetails.bankAccountHolderName || "");
984
+ apiFormData.append("bankAccountType", bankDetails.bankAccountType || "checking");
985
+ apiFormData.append("bankRoutingNumber", bankDetails.bankRoutingNumber || "");
986
+ apiFormData.append("bankAccountNumber", bankDetails.bankAccountNumber || "");
987
+
988
+ // Add underwriting documents (files)
989
+ const underwritingDocs = processedData.underwriting?.underwritingDocuments || [];
990
+ underwritingDocs.forEach((file) => {
991
+ if (file instanceof File) {
992
+ apiFormData.append("underwritingDocuments", file);
993
+ }
994
+ });
995
+
996
+ console.log("OperatorOnboarding: Calling registerOperator API");
997
+ console.log("OperatorOnboarding: FormData entries:");
998
+ for (const [key, value] of apiFormData.entries()) {
999
+ console.log(` ${key}:`, value instanceof File ? `File(${value.name})` : value);
1000
+ }
1001
+ const response = await this.api.registerOperator(apiFormData);
1002
+ console.log("OperatorOnboarding: registerOperator API response", response);
1003
+
1004
+ if (shouldFail || !response.success) {
1005
+ // Handle submission failure
1006
+ this.handleSubmissionFailure(processedData);
1007
+ return;
1008
+ }
1009
+
1010
+ // Update state to show success page
1011
+ this.setState({
1012
+ isSubmitted: true,
1013
+ uiState: { isLoading: false },
1014
+ });
1015
+
1016
+ // Emit custom event
1017
+ this.dispatchEvent(
1018
+ new CustomEvent("formComplete", {
1019
+ detail: { ...processedData, apiResponse: response },
1020
+ bubbles: true,
1021
+ composed: true,
1022
+ })
1023
+ );
1024
+
1025
+ // Call onSuccess callback if provided
1026
+ console.log("OperatorOnboarding: Checking onSuccess callback", {
1027
+ hasCallback: !!this.onSuccess,
1028
+ callbackType: typeof this.onSuccess,
1029
+ });
1030
+ if (this.onSuccess && typeof this.onSuccess === "function") {
1031
+ console.log("OperatorOnboarding: Calling onSuccess callback");
1032
+ this.onSuccess({ ...processedData, apiResponse: response });
1033
+ }
1034
+ } catch (error) {
1035
+ console.error("OperatorOnboarding: registerOperator API error", error);
1036
+ this.handleSubmissionFailure(processedData);
1037
+ }
1038
+ }
1039
+
1040
+ handleSubmissionFailure(formData) {
1041
+ const errorData = {
1042
+ formData,
1043
+ message: "Form submission failed. Please try again.",
1044
+ timestamp: new Date().toISOString(),
1045
+ };
1046
+
1047
+ // Log error to console
1048
+ console.error("Submission Failed:", errorData);
1049
+
1050
+ // Update state to show failure page
1051
+ this.setState({
1052
+ isSubmissionFailed: true,
1053
+ uiState: {
1054
+ ...this.state.uiState,
1055
+ isLoading: false,
1056
+ errorMessage: errorData.message,
1057
+ showErrors: false,
1058
+ },
1059
+ });
1060
+
1061
+ // Emit custom error event
1062
+ this.dispatchEvent(
1063
+ new CustomEvent("submissionFailed", {
1064
+ detail: errorData,
1065
+ bubbles: true,
1066
+ composed: true,
1067
+ })
1068
+ );
1069
+
1070
+ // Call onError callback if provided
1071
+ if (this.onError && typeof this.onError === "function") {
1072
+ this.onError(errorData);
1073
+ }
1074
+ }
1075
+
1076
+ /**
1077
+ * Handle success confirmation button click
1078
+ * Dispatches event and calls onConfirm callback, then closes modal
1079
+ */
1080
+ handleSuccessConfirm() {
1081
+ const confirmData = {
1082
+ formData: this.state.formData,
1083
+ timestamp: new Date().toISOString(),
1084
+ };
1085
+
1086
+ // Dispatch custom event
1087
+ this.dispatchEvent(
1088
+ new CustomEvent("onboardingConfirmed", {
1089
+ detail: confirmData,
1090
+ bubbles: true,
1091
+ composed: true,
1092
+ })
1093
+ );
1094
+
1095
+ // Call onConfirm callback if provided
1096
+ if (this.onConfirm && typeof this.onConfirm === "function") {
1097
+ this.onConfirm(confirmData);
1098
+ }
1099
+
1100
+ // Close the modal
1101
+ this.closeModal();
1102
+ }
1103
+
1104
+ // ==================== MODAL METHODS ====================
1105
+
1106
+ openModal() {
1107
+ this.setState({ isModalOpen: true });
1108
+
1109
+ // Apply animation classes after modal is rendered
1110
+ requestAnimationFrame(() => {
1111
+ const modal = this.shadowRoot.querySelector(".modal-overlay");
1112
+ if (modal) {
1113
+ modal.classList.add("show", "animating-in");
1114
+ setTimeout(() => {
1115
+ modal.classList.remove("animating-in");
1116
+ }, 200);
1117
+ }
1118
+ });
1119
+
1120
+ this.dispatchEvent(
1121
+ new CustomEvent("onboarding-modal-open", {
1122
+ bubbles: true,
1123
+ composed: true,
1124
+ })
1125
+ );
1126
+ }
1127
+
1128
+ closeModal() {
1129
+ // Clean up escape key handler
1130
+ if (this._escapeHandler) {
1131
+ document.removeEventListener("keydown", this._escapeHandler);
1132
+ this._escapeHandler = null;
1133
+ }
1134
+
1135
+ // Get the modal overlay for animation
1136
+ const overlay = this.shadowRoot.querySelector(".modal-overlay");
1137
+
1138
+ if (overlay) {
1139
+ // Add animating-out class to trigger exit animation
1140
+ overlay.classList.add("animating-out");
1141
+ overlay.classList.remove("show");
1142
+
1143
+ // Wait for animation to complete before removing from DOM
1144
+ setTimeout(() => {
1145
+ // Reset form to initial state (or onLoad values if provided)
1146
+ this.resetForm();
1147
+
1148
+ // Update state to remove modal from DOM
1149
+ this.setState({ isModalOpen: false });
1150
+
1151
+ // Restore body scroll
1152
+ document.body.style.overflow = "";
1153
+
1154
+ // Dispatch close event
1155
+ this.dispatchEvent(
1156
+ new CustomEvent("onboarding-modal-close", {
1157
+ bubbles: true,
1158
+ composed: true,
1159
+ })
1160
+ );
1161
+ }, 150); // Match the fadeOut animation duration (150ms)
1162
+ } else {
1163
+ // Reset form to initial state (or onLoad values if provided)
1164
+ this.resetForm();
1165
+
1166
+ // Fallback if overlay not found - close immediately
1167
+ this.setState({ isModalOpen: false });
1168
+ document.body.style.overflow = "";
1169
+ this.dispatchEvent(
1170
+ new CustomEvent("onboarding-modal-close", {
1171
+ bubbles: true,
1172
+ composed: true,
1173
+ })
1174
+ );
1175
+ }
1176
+ }
1177
+
1178
+ // ==================== RENDERING ====================
1179
+
1180
+ render() {
1181
+ // Always render the button + modal structure
1182
+ this.shadowRoot.innerHTML = `
1183
+ ${this.renderStyles()}
1184
+ ${this.renderButton()}
1185
+ ${this.state.isModalOpen ? this.renderModal() : ""}
1186
+ `;
1187
+ this.attachEventListeners();
1188
+ }
1189
+
1190
+ renderButton() {
1191
+ return `
1192
+ <button class="onboarding-trigger-btn" type="button">
1193
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1194
+ <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
1195
+ <circle cx="8.5" cy="7" r="4"></circle>
1196
+ <line x1="20" y1="8" x2="20" y2="14"></line>
1197
+ <line x1="23" y1="11" x2="17" y2="11"></line>
1198
+ </svg>
1199
+ Start Onboarding
1200
+ </button>
1201
+ `;
1202
+ }
1203
+
1204
+ renderModal() {
1205
+ // Add 'show' class if modal was already open to prevent flash during re-renders
1206
+ const showClass = this._skipModalAnimation ? "show" : "";
1207
+ // Hide close button during submission and on success screen (require confirm button)
1208
+ const hideCloseButton = this.state.uiState.isLoading || this.state.isSubmitted;
1209
+
1210
+ return `
1211
+ <div class="modal-overlay ${showClass}">
1212
+ <div class="modal-container">
1213
+ ${!hideCloseButton ? `
1214
+ <button class="modal-close-btn" type="button" aria-label="Close modal">
1215
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1216
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1217
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1218
+ </svg>
1219
+ </button>
1220
+ ` : ''}
1221
+ ${this.renderModalContent()}
1222
+ </div>
1223
+ </div>
1224
+ `;
1225
+ }
1226
+
1227
+ renderModalContent() {
1228
+ // Show submission failure page
1229
+ if (this.state.isSubmissionFailed) {
1230
+ return `
1231
+ <div class="modal-body-full">
1232
+ ${this.renderSubmissionFailurePage()}
1233
+ </div>
1234
+ `;
1235
+ }
1236
+
1237
+ // Show success page if form is submitted
1238
+ if (this.state.isSubmitted) {
1239
+ return `
1240
+ <div class="modal-body-full">
1241
+ ${this.renderSuccessPage()}
1242
+ </div>
1243
+ `;
1244
+ }
1245
+
1246
+ // Show loading during submission
1247
+ if (this.state.uiState.isLoading) {
1248
+ return `
1249
+ <div class="modal-body-full">
1250
+ <div class="loading-content">
1251
+ <h2>Submitting Your Application...</h2>
1252
+ <p style="color: var(--gray-medium); margin-bottom: var(--spacing-lg);">
1253
+ Please wait while we process your information.
1254
+ </p>
1255
+ <div class="loading-spinner"></div>
1256
+ </div>
1257
+ </div>
1258
+ `;
1259
+ }
1260
+
1261
+ // Show main stepper form with fixed header/footer layout
1262
+ return `
1263
+ <div class="modal-layout">
1264
+ <div class="modal-header">
1265
+ <div class="form-logo">
1266
+ <img src="https://bisonpaywell.com/lovable-uploads/28831244-e8b3-4e7b-8dbb-c016f9f9d54f.png" alt="Logo" />
1267
+ </div>
1268
+ ${this.renderStepperHeader()}
1269
+ </div>
1270
+ <div class="modal-body">
1271
+ ${this.renderFormContent()}
1272
+ </div>
1273
+ <div class="modal-footer">
1274
+ ${this.renderNavigationFooter()}
1275
+ </div>
1276
+ </div>
1277
+ `;
1278
+ }
1279
+
1280
+ renderFormContent() {
1281
+ const stepId = this.STEPS[this.state.currentStep].id;
1282
+
1283
+ switch (stepId) {
1284
+ case "business-details":
1285
+ return this.renderBusinessDetailsForm();
1286
+ case "representatives":
1287
+ return this.renderRepresentativesForm();
1288
+ case "bank-details":
1289
+ return this.renderBankDetailsForm();
1290
+ case "underwriting":
1291
+ return this.renderUnderwritingForm();
1292
+ default:
1293
+ return "";
1294
+ }
1295
+ }
1296
+
1297
+ renderStyles() {
1298
+ return `
1299
+ <style>
1300
+ * {
1301
+ box-sizing: border-box;
1302
+ margin: 0;
1303
+ padding: 0;
1304
+ }
1305
+
1306
+ :host {
1307
+ --primary-color: var(--color-primary, #4c7b63);
1308
+ --success-color: var(--color-success, #22c55e);
1309
+ --error-color: var(--color-error, #dd524b);
1310
+ --border-color: var(--color-border, #e8e8e8);
1311
+ --gray-light: var(--color-gray-50, #f9fafb);
1312
+ --gray-medium: var(--color-gray-500, #6b7280);
1313
+ --border-radius: var(--radius-xl, 0.75rem);
1314
+ --border-radius-sm: var(--radius-lg, 0.5rem);
1315
+ --border-radius-lg: var(--radius-2xl, 1rem);
1316
+ --spacing-sm: var(--spacing-sm, 0.5rem);
1317
+ --spacing-md: var(--spacing-md, 1rem);
1318
+ --spacing-lg: var(--spacing-lg, 1.5rem);
1319
+ font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
1320
+ color: var(--color-secondary, #5f6e78);
1321
+ display: inline-block;
1322
+ }
1323
+
1324
+ /* Trigger Button */
1325
+ .onboarding-trigger-btn {
1326
+ display: inline-flex;
1327
+ align-items: center;
1328
+ gap: 8px;
1329
+ padding: 12px 24px;
1330
+ background: var(--primary-color);
1331
+ color: var(--color-white, #fff);
1332
+ border: none;
1333
+ border-radius: var(--border-radius);
1334
+ font-size: var(--text-sm, 0.875rem);
1335
+ font-weight: var(--font-weight-medium, 500);
1336
+ cursor: pointer;
1337
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1338
+ height: 40px;
1339
+ box-sizing: border-box;
1340
+ }
1341
+
1342
+ .onboarding-trigger-btn:hover {
1343
+ background: var(--color-primary-hover, #436c57);
1344
+ }
1345
+
1346
+ .onboarding-trigger-btn:active {
1347
+ transform: translateY(0);
1348
+ }
1349
+
1350
+ .onboarding-trigger-btn svg {
1351
+ flex-shrink: 0;
1352
+ }
1353
+
1354
+ /* Modal Overlay */
1355
+ .modal-overlay {
1356
+ position: fixed;
1357
+ top: 0;
1358
+ left: 0;
1359
+ right: 0;
1360
+ bottom: 0;
1361
+ background: rgba(0, 0, 0, 0.5);
1362
+ display: flex;
1363
+ align-items: center;
1364
+ justify-content: center;
1365
+ z-index: 10000;
1366
+ padding: 20px;
1367
+ opacity: 0;
1368
+ }
1369
+
1370
+ .modal-overlay.show {
1371
+ opacity: 1;
1372
+ }
1373
+
1374
+ .modal-overlay.animating-in {
1375
+ animation: fadeIn 200ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
1376
+ }
1377
+
1378
+ .modal-overlay.animating-out {
1379
+ animation: fadeOut 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
1380
+ }
1381
+
1382
+ @keyframes fadeIn {
1383
+ from { opacity: 0; }
1384
+ to { opacity: 1; }
1385
+ }
1386
+
1387
+ @keyframes fadeOut {
1388
+ from { opacity: 1; }
1389
+ to { opacity: 0; }
1390
+ }
1391
+
1392
+ /* Modal Container */
1393
+ .modal-container {
1394
+ background: var(--color-white, #fff);
1395
+ border-radius: var(--border-radius-lg);
1396
+ max-width: 900px;
1397
+ width: 100%;
1398
+ max-height: 90vh;
1399
+ overflow: hidden;
1400
+ position: relative;
1401
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1402
+ display: flex;
1403
+ flex-direction: column;
1404
+ opacity: 0;
1405
+ transform: scale(0.95) translateY(-10px);
1406
+ }
1407
+
1408
+ .modal-overlay.show .modal-container {
1409
+ opacity: 1;
1410
+ transform: scale(1) translateY(0);
1411
+ }
1412
+
1413
+ .modal-overlay.animating-in .modal-container {
1414
+ animation: slideInScale 200ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
1415
+ }
1416
+
1417
+ .modal-overlay.animating-out .modal-container {
1418
+ animation: slideOutScale 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
1419
+ }
1420
+
1421
+ @keyframes slideInScale {
1422
+ from {
1423
+ opacity: 0;
1424
+ transform: scale(0.95) translateY(-10px);
1425
+ }
1426
+ to {
1427
+ opacity: 1;
1428
+ transform: scale(1) translateY(0);
1429
+ }
1430
+ }
1431
+
1432
+ @keyframes slideOutScale {
1433
+ from {
1434
+ opacity: 1;
1435
+ transform: scale(1) translateY(0);
1436
+ }
1437
+ to {
1438
+ opacity: 0;
1439
+ transform: scale(0.95) translateY(-10px);
1440
+ }
1441
+ }
1442
+
1443
+ /* Modal Layout - Fixed Header/Footer with Scrollable Body */
1444
+ .modal-layout {
1445
+ display: flex;
1446
+ flex-direction: column;
1447
+ height: 100%;
1448
+ max-height: 90vh;
1449
+ overflow: hidden;
1450
+ }
1451
+
1452
+ .modal-header {
1453
+ flex-shrink: 0;
1454
+ padding: var(--spacing-lg);
1455
+ border-bottom: 1px solid var(--border-color);
1456
+ background: var(--color-white, #fff);
1457
+ }
1458
+
1459
+ .modal-body {
1460
+ flex: 1;
1461
+ overflow-y: auto;
1462
+ padding: var(--spacing-lg);
1463
+ min-height: 0;
1464
+ }
1465
+
1466
+ .modal-footer {
1467
+ flex-shrink: 0;
1468
+ padding: var(--spacing-lg);
1469
+ border-top: 1px solid var(--border-color);
1470
+ background: var(--color-white, #fff);
1471
+ }
1472
+
1473
+ .modal-body-full {
1474
+ padding: var(--spacing-lg);
1475
+ overflow-y: auto;
1476
+ max-height: calc(90vh - 60px);
1477
+ }
1478
+
1479
+ .loading-content {
1480
+ text-align: center;
1481
+ padding: calc(var(--spacing-lg) * 2);
1482
+ }
1483
+
1484
+
1485
+ /* Modal Close Button */
1486
+ .modal-close-btn {
1487
+ position: absolute;
1488
+ top: 16px;
1489
+ right: 16px;
1490
+ background: none;
1491
+ border: none;
1492
+ cursor: pointer;
1493
+ padding: 8px;
1494
+ border-radius: 50%;
1495
+ color: var(--gray-medium);
1496
+ transition: all 0.2s ease;
1497
+ z-index: 10;
1498
+ }
1499
+
1500
+ .modal-close-btn:hover {
1501
+ background: var(--gray-light);
1502
+ color: var(--color-headline, #0f2a39);
1503
+ }
1504
+
1505
+ .onboarding-container {
1506
+ max-width: 900px;
1507
+ margin: 0 auto;
1508
+ padding: var(--spacing-lg);
1509
+ }
1510
+
1511
+ /* Logo inside modal header */
1512
+ .form-logo {
1513
+ text-align: center;
1514
+ margin-bottom: var(--spacing-md);
1515
+ }
1516
+
1517
+ .form-logo img {
1518
+ max-width: 140px;
1519
+ height: auto;
1520
+ }
1521
+
1522
+ /* Stepper Header */
1523
+ .stepper-header {
1524
+ display: flex;
1525
+ justify-content: space-between;
1526
+ position: relative;
1527
+ }
1528
+
1529
+ .stepper-header::before {
1530
+ content: '';
1531
+ position: absolute;
1532
+ top: 20px;
1533
+ left: 0;
1534
+ right: 0;
1535
+ height: 2px;
1536
+ background: var(--border-color);
1537
+ z-index: 0;
1538
+ }
1539
+
1540
+ .step-indicator {
1541
+ flex: 1;
1542
+ text-align: center;
1543
+ position: relative;
1544
+ z-index: 1;
1545
+ }
1546
+
1547
+ .step-indicator.clickable {
1548
+ cursor: pointer;
1549
+ }
1550
+
1551
+ .step-circle {
1552
+ width: 40px;
1553
+ height: 40px;
1554
+ border-radius: 50%;
1555
+ background: var(--color-white, #fff);
1556
+ border: 2px solid var(--border-color);
1557
+ display: flex;
1558
+ align-items: center;
1559
+ justify-content: center;
1560
+ margin: 0 auto var(--spacing-sm);
1561
+ font-weight: bold;
1562
+ color: var(--gray-medium);
1563
+ }
1564
+
1565
+ .step-indicator.active .step-circle {
1566
+ border-color: var(--primary-color);
1567
+ color: var(--primary-color);
1568
+ background: var(--primary-color);
1569
+ color: var(--color-white, #fff);
1570
+ }
1571
+
1572
+ .step-indicator.complete .step-circle {
1573
+ border-color: var(--success-color);
1574
+ background: var(--success-color);
1575
+ color: var(--color-white, #fff);
1576
+ }
1577
+
1578
+ .step-label {
1579
+ font-size: 12px;
1580
+ color: var(--gray-medium);
1581
+ }
1582
+
1583
+ .step-indicator.active .step-label {
1584
+ color: var(--primary-color);
1585
+ font-weight: 600;
1586
+ }
1587
+
1588
+ /* Step Content */
1589
+ .step-content {
1590
+ background: var(--color-white, #fff);
1591
+ padding: calc(var(--spacing-lg) * 1.5);
1592
+ border: 1px solid var(--border-color);
1593
+ border-radius: var(--border-radius-lg);
1594
+ margin-bottom: var(--spacing-lg);
1595
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1596
+ }
1597
+
1598
+ .step-content h2 {
1599
+ margin-bottom: var(--spacing-sm);
1600
+ color: var(--color-headline, #0f2a39);
1601
+ }
1602
+
1603
+ .step-content > p {
1604
+ color: var(--gray-medium);
1605
+ margin-bottom: var(--spacing-lg);
1606
+ }
1607
+
1608
+ /* Form Section - for scrollable content in modal */
1609
+ .form-section {
1610
+ background: var(--color-white, #fff);
1611
+ }
1612
+
1613
+ .form-section h2 {
1614
+ margin-bottom: var(--spacing-sm);
1615
+ color: var(--color-headline, #0f2a39);
1616
+ }
1617
+
1618
+ .form-section > p {
1619
+ color: var(--gray-medium);
1620
+ margin-bottom: var(--spacing-lg);
1621
+ }
1622
+
1623
+ /* Form Fields */
1624
+ .form-field {
1625
+ margin-bottom: var(--spacing-md);
1626
+ }
1627
+
1628
+ .form-field label {
1629
+ display: block;
1630
+ margin-bottom: var(--spacing-sm);
1631
+ font-weight: 500;
1632
+ color: var(--color-headline, #0f2a39);
1633
+ }
1634
+
1635
+ /* Red asterisk for required fields */
1636
+ .required-asterisk {
1637
+ color: var(--error-color);
1638
+ font-weight: bold;
1639
+ }
1640
+
1641
+ .form-field input,
1642
+ .form-field select {
1643
+ width: 100%;
1644
+ padding: 10px;
1645
+ border: 1px solid var(--border-color);
1646
+ border-radius: var(--border-radius-sm);
1647
+ font-size: 14px;
1648
+ }
1649
+
1650
+ .form-field input:focus,
1651
+ .form-field select:focus {
1652
+ outline: none;
1653
+ border-color: var(--primary-color);
1654
+ }
1655
+
1656
+ .form-field input[readonly] {
1657
+ background: var(--gray-light);
1658
+ cursor: not-allowed;
1659
+ }
1660
+
1661
+ .form-field.has-error input,
1662
+ .form-field.has-error select {
1663
+ border-color: var(--error-color);
1664
+ }
1665
+
1666
+ .error-message {
1667
+ display: block;
1668
+ color: var(--error-color);
1669
+ font-size: 12px;
1670
+ margin-top: var(--spacing-sm);
1671
+ }
1672
+
1673
+ .form-grid {
1674
+ display: grid;
1675
+ grid-template-columns: 1fr 1fr;
1676
+ gap: var(--spacing-md);
1677
+ }
1678
+
1679
+ .form-grid .full-width {
1680
+ grid-column: 1 / -1;
1681
+ }
1682
+
1683
+ /* Radio Buttons */
1684
+ .radio-group {
1685
+ display: flex;
1686
+ gap: var(--spacing-md);
1687
+ }
1688
+
1689
+ .radio-option {
1690
+ display: flex;
1691
+ align-items: center;
1692
+ gap: var(--spacing-sm);
1693
+ }
1694
+
1695
+ .radio-option input[type="radio"] {
1696
+ width: auto;
1697
+ }
1698
+
1699
+ /* Representatives */
1700
+ .representative-card {
1701
+ border: 1px solid var(--border-color);
1702
+ border-radius: var(--border-radius-lg);
1703
+ padding: var(--spacing-md);
1704
+ margin-bottom: var(--spacing-md);
1705
+ }
1706
+
1707
+ .card-header {
1708
+ display: flex;
1709
+ justify-content: space-between;
1710
+ align-items: center;
1711
+ margin-bottom: var(--spacing-md);
1712
+ padding-bottom: var(--spacing-sm);
1713
+ border-bottom: 1px solid var(--border-color);
1714
+ }
1715
+
1716
+ .card-header h3 {
1717
+ font-size: 16px;
1718
+ color: var(--color-headline, #0f2a39);
1719
+ }
1720
+
1721
+ .remove-btn {
1722
+ background: none;
1723
+ border: none;
1724
+ color: var(--error-color);
1725
+ cursor: pointer;
1726
+ font-size: 14px;
1727
+ padding: var(--spacing-sm);
1728
+ }
1729
+
1730
+ .remove-btn:hover {
1731
+ text-decoration: underline;
1732
+ }
1733
+
1734
+ .add-representative-btn {
1735
+ width: 100%;
1736
+ padding: 12px;
1737
+ background: var(--color-white, #fff);
1738
+ border: 2px dashed var(--border-color);
1739
+ border-radius: var(--border-radius);
1740
+ color: var(--primary-color);
1741
+ cursor: pointer;
1742
+ font-size: 14px;
1743
+ font-weight: 500;
1744
+ }
1745
+
1746
+ .add-representative-btn:hover {
1747
+ border-color: var(--primary-color);
1748
+ background: var(--gray-light);
1749
+ }
1750
+
1751
+ /* Navigation Footer */
1752
+ .navigation-footer {
1753
+ display: flex;
1754
+ justify-content: space-between;
1755
+ gap: var(--spacing-md);
1756
+ margin: 0;
1757
+ }
1758
+
1759
+ .navigation-footer button {
1760
+ padding: 12px 24px;
1761
+ border: none;
1762
+ border-radius: var(--border-radius);
1763
+ font-size: 14px;
1764
+ font-weight: 500;
1765
+ cursor: pointer;
1766
+ height: 40px;
1767
+ box-sizing: border-box;
1768
+ }
1769
+
1770
+ .btn-back {
1771
+ background: var(--color-white, #fff);
1772
+ border: 1px solid var(--border-color);
1773
+ color: var(--color-headline, #0f2a39);
1774
+ }
1775
+
1776
+ .btn-back:hover {
1777
+ background: var(--gray-light);
1778
+ }
1779
+
1780
+ .btn-skip {
1781
+ background: var(--color-white, #fff);
1782
+ border: 1px solid var(--border-color);
1783
+ color: var(--gray-medium);
1784
+ margin-left: auto;
1785
+ }
1786
+
1787
+ .btn-skip:hover {
1788
+ background: var(--gray-light);
1789
+ }
1790
+
1791
+ .btn-next {
1792
+ background: var(--primary-color);
1793
+ color: var(--color-white, #fff);
1794
+ }
1795
+
1796
+ .btn-next:hover {
1797
+ background: var(--color-primary-hover, #436c57);
1798
+ }
1799
+
1800
+ /* Success Confirmation Button */
1801
+ .btn-confirm-success {
1802
+ margin-top: var(--spacing-lg);
1803
+ padding: 14px 32px;
1804
+ background: var(--primary-color);
1805
+ color: var(--color-white, #fff);
1806
+ border: none;
1807
+ border-radius: var(--border-radius);
1808
+ font-size: 16px;
1809
+ font-weight: var(--font-weight-medium, 500);
1810
+ cursor: pointer;
1811
+ transition: all var(--duration-normal, 200ms) var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
1812
+ }
1813
+
1814
+ .btn-confirm-success:hover {
1815
+ background: var(--color-primary-hover, #436c57);
1816
+ }
1817
+
1818
+ .btn-confirm-success:active {
1819
+ transform: translateY(0);
1820
+ }
1821
+
1822
+ /* Loading & Messages */
1823
+ .loading-spinner {
1824
+ border: 3px solid var(--gray-light);
1825
+ border-top: 3px solid var(--primary-color);
1826
+ border-radius: 50%;
1827
+ width: 40px;
1828
+ height: 40px;
1829
+ animation: spin 1s linear infinite;
1830
+ margin: var(--spacing-md) auto;
1831
+ }
1832
+
1833
+ @keyframes spin {
1834
+ 0% { transform: rotate(0deg); }
1835
+ 100% { transform: rotate(360deg); }
1836
+ }
1837
+
1838
+ .success-message {
1839
+ background: var(--color-success-light, #d3f3df);
1840
+ color: var(--color-success-dark, #136c34);
1841
+ padding: var(--spacing-md);
1842
+ border-radius: var(--border-radius);
1843
+ margin-top: var(--spacing-md);
1844
+ border: 1px solid var(--color-success-light, #d3f3df);
1845
+ }
1846
+
1847
+ .empty-state {
1848
+ text-align: center;
1849
+ padding: var(--spacing-lg);
1850
+ color: var(--gray-medium);
1851
+ }
1852
+
1853
+ /* Drag and Drop Styles */
1854
+ .drag-drop-area {
1855
+ border: 2px dashed var(--border-color);
1856
+ border-radius: var(--border-radius-lg);
1857
+ padding: calc(var(--spacing-lg) * 2);
1858
+ text-align: center;
1859
+ background: var(--gray-light);
1860
+ transition: all 0.3s ease;
1861
+ cursor: pointer;
1862
+ }
1863
+
1864
+ .drag-drop-area:hover {
1865
+ border-color: var(--primary-color);
1866
+ background: var(--color-primary-light, #e8f0eb);
1867
+ }
1868
+
1869
+ .drag-drop-area.drag-over {
1870
+ border-color: var(--primary-color);
1871
+ background: var(--color-primary-light, #e8f0eb);
1872
+ border-style: solid;
1873
+ }
1874
+
1875
+ .drag-drop-content {
1876
+ pointer-events: none;
1877
+ }
1878
+
1879
+ .btn-browse:hover {
1880
+ background: var(--color-primary-hover, #436c57);
1881
+ }
1882
+
1883
+ .uploaded-files {
1884
+ margin-top: var(--spacing-md);
1885
+ }
1886
+
1887
+ .file-item:hover {
1888
+ background: var(--color-gray-100, #f3f4f6);
1889
+ }
1890
+
1891
+ .btn-remove-file:hover {
1892
+ text-decoration: underline;
1893
+ }
1894
+
1895
+ /* Error/Failure Styles */
1896
+ .error-container {
1897
+ text-align: center;
1898
+ padding: var(--spacing-lg) 0;
1899
+ }
1900
+
1901
+ .error-icon {
1902
+ width: 120px;
1903
+ height: 120px;
1904
+ margin: 0 auto var(--spacing-lg);
1905
+ background: linear-gradient(135deg, var(--error-color) 0%, var(--color-error-dark, #903531) 100%);
1906
+ border-radius: 50%;
1907
+ display: flex;
1908
+ align-items: center;
1909
+ justify-content: center;
1910
+ animation: errorPulse 0.6s ease-out;
1911
+ }
1912
+
1913
+ @keyframes errorPulse {
1914
+ 0% {
1915
+ transform: scale(0);
1916
+ opacity: 0;
1917
+ }
1918
+ 50% {
1919
+ transform: scale(1.1);
1920
+ }
1921
+ 100% {
1922
+ transform: scale(1);
1923
+ opacity: 1;
1924
+ }
1925
+ }
1926
+
1927
+ .error-icon svg {
1928
+ width: 70px;
1929
+ height: 70px;
1930
+ stroke: var(--color-white, #fff);
1931
+ stroke-width: 3;
1932
+ stroke-linecap: round;
1933
+ stroke-linejoin: round;
1934
+ fill: none;
1935
+ }
1936
+
1937
+ .error-container h2 {
1938
+ color: var(--error-color);
1939
+ margin-bottom: var(--spacing-md);
1940
+ font-size: 32px;
1941
+ }
1942
+
1943
+ .error-container p {
1944
+ color: var(--gray-medium);
1945
+ font-size: 16px;
1946
+ line-height: 1.6;
1947
+ margin-bottom: var(--spacing-sm);
1948
+ }
1949
+
1950
+ .error-details {
1951
+ background: var(--color-error-light, #fae5e4);
1952
+ border: 1px solid var(--color-error-muted, #eea9a5);
1953
+ border-radius: var(--border-radius-lg);
1954
+ padding: var(--spacing-lg);
1955
+ margin: var(--spacing-lg) 0;
1956
+ text-align: left;
1957
+ }
1958
+
1959
+ .error-details h3 {
1960
+ color: var(--error-color);
1961
+ margin-bottom: var(--spacing-md);
1962
+ font-size: 18px;
1963
+ }
1964
+
1965
+ .error-details p {
1966
+ color: var(--color-error-dark, #903531);
1967
+ margin-bottom: var(--spacing-sm);
1968
+ }
1969
+
1970
+ .btn-fail {
1971
+ background: var(--error-color);
1972
+ color: var(--color-white, #fff);
1973
+ padding: 12px 24px;
1974
+ border: none;
1975
+ border-radius: var(--border-radius);
1976
+ font-size: 14px;
1977
+ font-weight: 500;
1978
+ cursor: pointer;
1979
+ margin-top: var(--spacing-md);
1980
+ margin-left: var(--spacing-sm);
1981
+ }
1982
+
1983
+ .btn-fail:hover {
1984
+ background: var(--color-error-dark, #903531);
1985
+ }
1986
+
1987
+ /* Success Page */
1988
+ .success-container {
1989
+ text-align: center;
1990
+ padding: var(--spacing-lg) 0;
1991
+ }
1992
+
1993
+ .success-icon {
1994
+ width: 120px;
1995
+ height: 120px;
1996
+ margin: 0 auto var(--spacing-lg);
1997
+ background: linear-gradient(135deg, var(--success-color) 0%, var(--color-success-dark, #136c34) 100%);
1998
+ border-radius: 50%;
1999
+ display: flex;
2000
+ align-items: center;
2001
+ justify-content: center;
2002
+ animation: successPulse 0.6s ease-out;
2003
+ }
2004
+
2005
+ @keyframes successPulse {
2006
+ 0% {
2007
+ transform: scale(0);
2008
+ opacity: 0;
2009
+ }
2010
+ 50% {
2011
+ transform: scale(1.1);
2012
+ }
2013
+ 100% {
2014
+ transform: scale(1);
2015
+ opacity: 1;
2016
+ }
2017
+ }
2018
+
2019
+ .success-icon svg {
2020
+ width: 70px;
2021
+ height: 70px;
2022
+ stroke: var(--color-white, #fff);
2023
+ stroke-width: 3;
2024
+ stroke-linecap: round;
2025
+ stroke-linejoin: round;
2026
+ fill: none;
2027
+ stroke-dasharray: 100;
2028
+ stroke-dashoffset: 100;
2029
+ animation: checkmark 0.6s ease-out 0.3s forwards;
2030
+ }
2031
+
2032
+ @keyframes checkmark {
2033
+ to {
2034
+ stroke-dashoffset: 0;
2035
+ }
2036
+ }
2037
+
2038
+ .success-container h2 {
2039
+ color: var(--success-color);
2040
+ margin-bottom: var(--spacing-md);
2041
+ font-size: 32px;
2042
+ }
2043
+
2044
+ .success-container p {
2045
+ color: var(--gray-medium);
2046
+ font-size: 16px;
2047
+ line-height: 1.6;
2048
+ margin-bottom: var(--spacing-sm);
2049
+ }
2050
+
2051
+ .success-details {
2052
+ background: var(--gray-light);
2053
+ border-radius: var(--border-radius-lg);
2054
+ padding: var(--spacing-lg);
2055
+ margin: var(--spacing-lg) 0;
2056
+ text-align: left;
2057
+ }
2058
+
2059
+ .success-details h3 {
2060
+ color: var(--color-headline, #0f2a39);
2061
+ margin-bottom: var(--spacing-md);
2062
+ font-size: 18px;
2063
+ }
2064
+
2065
+ .detail-item {
2066
+ display: flex;
2067
+ justify-content: space-between;
2068
+ padding: var(--spacing-sm) 0;
2069
+ border-bottom: 1px solid var(--border-color);
2070
+ }
2071
+
2072
+ .detail-item:last-child {
2073
+ border-bottom: none;
2074
+ }
2075
+
2076
+ .detail-label {
2077
+ color: var(--gray-medium);
2078
+ font-size: 14px;
2079
+ }
2080
+
2081
+ .detail-value {
2082
+ color: var(--color-headline, #0f2a39);
2083
+ font-weight: 500;
2084
+ font-size: 14px;
2085
+ }
2086
+
2087
+ /* ==================== MOBILE RESPONSIVE STYLES ==================== */
2088
+
2089
+ /* Tablet breakpoint (768px and below) */
2090
+ @media screen and (max-width: 768px) {
2091
+ .modal-overlay {
2092
+ padding: 10px;
2093
+ }
2094
+
2095
+ .modal-container {
2096
+ max-height: 95vh;
2097
+ border-radius: var(--border-radius);
2098
+ }
2099
+
2100
+ .modal-header {
2101
+ padding: var(--spacing-md);
2102
+ }
2103
+
2104
+ .modal-body {
2105
+ padding: var(--spacing-md);
2106
+ }
2107
+
2108
+ .modal-footer {
2109
+ padding: var(--spacing-md);
2110
+ }
2111
+
2112
+ .modal-body-full {
2113
+ padding: var(--spacing-md);
2114
+ max-height: calc(95vh - 50px);
2115
+ }
2116
+
2117
+ .modal-close-btn {
2118
+ top: 12px;
2119
+ right: 12px;
2120
+ padding: 6px;
2121
+ }
2122
+
2123
+ /* Stepper - show only current step label on tablet */
2124
+ .stepper-header {
2125
+ gap: var(--spacing-sm);
2126
+ }
2127
+
2128
+ .step-circle {
2129
+ width: 36px;
2130
+ height: 36px;
2131
+ font-size: 14px;
2132
+ }
2133
+
2134
+ .step-label {
2135
+ font-size: 11px;
2136
+ }
2137
+
2138
+ /* Form grid - single column on tablet */
2139
+ .form-grid {
2140
+ grid-template-columns: 1fr;
2141
+ gap: var(--spacing-sm);
2142
+ }
2143
+
2144
+ .step-content {
2145
+ padding: var(--spacing-md);
2146
+ }
2147
+
2148
+ .step-content h2 {
2149
+ font-size: 20px;
2150
+ }
2151
+
2152
+ /* Representative cards */
2153
+ .representative-card {
2154
+ padding: var(--spacing-sm);
2155
+ }
2156
+
2157
+ .card-header h3 {
2158
+ font-size: 14px;
2159
+ }
2160
+
2161
+ /* Navigation footer */
2162
+ .navigation-footer {
2163
+ flex-wrap: wrap;
2164
+ gap: var(--spacing-sm);
2165
+ }
2166
+
2167
+ .navigation-footer button {
2168
+ padding: 10px 16px;
2169
+ font-size: 13px;
2170
+ height: 38px;
2171
+ }
2172
+
2173
+ /* Drag drop area */
2174
+ .drag-drop-area {
2175
+ padding: var(--spacing-lg);
2176
+ }
2177
+
2178
+ /* Success/Error containers */
2179
+ .success-icon,
2180
+ .error-icon {
2181
+ width: 100px;
2182
+ height: 100px;
2183
+ }
2184
+
2185
+ .success-container h2,
2186
+ .error-container h2 {
2187
+ font-size: 24px;
2188
+ }
2189
+
2190
+ .success-details {
2191
+ padding: var(--spacing-md);
2192
+ }
2193
+ }
2194
+
2195
+ /* Mobile breakpoint (480px and below) */
2196
+ @media screen and (max-width: 480px) {
2197
+ .modal-overlay {
2198
+ padding: 0;
2199
+ }
2200
+
2201
+ .modal-container {
2202
+ max-height: 100vh;
2203
+ height: 100vh;
2204
+ border-radius: 0;
2205
+ }
2206
+
2207
+ .modal-layout {
2208
+ max-height: 100vh;
2209
+ }
2210
+
2211
+ .modal-body-full {
2212
+ max-height: calc(100vh - 50px);
2213
+ }
2214
+
2215
+ .modal-header {
2216
+ padding: var(--spacing-sm) var(--spacing-md);
2217
+ }
2218
+
2219
+ .modal-body {
2220
+ padding: var(--spacing-sm) var(--spacing-md);
2221
+ }
2222
+
2223
+ .modal-footer {
2224
+ padding: var(--spacing-sm) var(--spacing-md);
2225
+ }
2226
+
2227
+ .modal-close-btn {
2228
+ top: 8px;
2229
+ right: 8px;
2230
+ }
2231
+
2232
+ /* Stepper - compact mobile version */
2233
+ .stepper-header {
2234
+ justify-content: center;
2235
+ gap: 4px;
2236
+ }
2237
+
2238
+ .stepper-header::before {
2239
+ top: 16px;
2240
+ }
2241
+
2242
+ .step-indicator {
2243
+ flex: 0 0 auto;
2244
+ min-width: 50px;
2245
+ }
2246
+
2247
+ .step-circle {
2248
+ width: 32px;
2249
+ height: 32px;
2250
+ font-size: 12px;
2251
+ }
2252
+
2253
+ .step-label {
2254
+ font-size: 10px;
2255
+ white-space: nowrap;
2256
+ overflow: hidden;
2257
+ text-overflow: ellipsis;
2258
+ max-width: 60px;
2259
+ }
2260
+
2261
+ /* Form fields */
2262
+ .form-field {
2263
+ margin-bottom: var(--spacing-sm);
2264
+ }
2265
+
2266
+ .form-field label {
2267
+ font-size: 13px;
2268
+ margin-bottom: 4px;
2269
+ }
2270
+
2271
+ .form-field input,
2272
+ .form-field select {
2273
+ padding: 8px;
2274
+ font-size: 16px; /* Prevents iOS zoom on focus */
2275
+ }
2276
+
2277
+ .step-content {
2278
+ padding: var(--spacing-sm);
2279
+ margin-bottom: var(--spacing-sm);
2280
+ }
2281
+
2282
+ .step-content h2 {
2283
+ font-size: 18px;
2284
+ }
2285
+
2286
+ .step-content > p {
2287
+ font-size: 13px;
2288
+ margin-bottom: var(--spacing-sm);
2289
+ }
2290
+
2291
+ .form-section h2 {
2292
+ font-size: 18px;
2293
+ }
2294
+
2295
+ .form-section > p {
2296
+ font-size: 13px;
2297
+ }
2298
+
2299
+ /* Representative cards */
2300
+ .representative-card {
2301
+ padding: var(--spacing-sm);
2302
+ margin-bottom: var(--spacing-sm);
2303
+ }
2304
+
2305
+ .card-header {
2306
+ margin-bottom: var(--spacing-sm);
2307
+ padding-bottom: 4px;
2308
+ }
2309
+
2310
+ .card-header h3 {
2311
+ font-size: 13px;
2312
+ }
2313
+
2314
+ .remove-btn {
2315
+ font-size: 12px;
2316
+ padding: 4px;
2317
+ }
2318
+
2319
+ .add-representative-btn {
2320
+ padding: 10px;
2321
+ font-size: 13px;
2322
+ }
2323
+
2324
+ /* Navigation footer - stack buttons on mobile */
2325
+ .navigation-footer {
2326
+ flex-direction: column-reverse;
2327
+ gap: var(--spacing-sm);
2328
+ }
2329
+
2330
+ .navigation-footer button {
2331
+ width: 100%;
2332
+ padding: 12px;
2333
+ font-size: 14px;
2334
+ height: 44px; /* Touch-friendly height */
2335
+ }
2336
+
2337
+ .btn-skip {
2338
+ margin-left: 0;
2339
+ }
2340
+
2341
+ /* Radio group - stack on mobile */
2342
+ .radio-group {
2343
+ flex-direction: column;
2344
+ gap: var(--spacing-sm);
2345
+ }
2346
+
2347
+ /* Drag drop area */
2348
+ .drag-drop-area {
2349
+ padding: var(--spacing-md);
2350
+ }
2351
+
2352
+ .drag-drop-content svg {
2353
+ width: 40px;
2354
+ height: 40px;
2355
+ }
2356
+
2357
+ /* File items */
2358
+ .file-item {
2359
+ flex-direction: column;
2360
+ align-items: flex-start;
2361
+ gap: var(--spacing-sm);
2362
+ }
2363
+
2364
+ /* Success/Error containers */
2365
+ .success-icon,
2366
+ .error-icon {
2367
+ width: 80px;
2368
+ height: 80px;
2369
+ }
2370
+
2371
+ .success-icon svg,
2372
+ .error-icon svg {
2373
+ width: 40px;
2374
+ height: 40px;
2375
+ }
2376
+
2377
+ .success-container h2,
2378
+ .error-container h2 {
2379
+ font-size: 20px;
2380
+ }
2381
+
2382
+ .success-container p,
2383
+ .error-container p {
2384
+ font-size: 14px;
2385
+ }
2386
+
2387
+ .success-details {
2388
+ padding: var(--spacing-sm);
2389
+ margin: var(--spacing-md) 0;
2390
+ }
2391
+
2392
+ .success-details h3 {
2393
+ font-size: 16px;
2394
+ }
2395
+
2396
+ .detail-item {
2397
+ flex-direction: column;
2398
+ gap: 4px;
2399
+ }
2400
+
2401
+ .detail-label,
2402
+ .detail-value {
2403
+ font-size: 13px;
2404
+ }
2405
+
2406
+ /* Trigger button - full width on mobile */
2407
+ .onboarding-trigger-btn {
2408
+ width: 100%;
2409
+ justify-content: center;
2410
+ }
2411
+ }
2412
+
2413
+ /* Small mobile breakpoint (320px and below) */
2414
+ @media screen and (max-width: 320px) {
2415
+ .step-label {
2416
+ display: none;
2417
+ }
2418
+
2419
+ .step-circle {
2420
+ width: 28px;
2421
+ height: 28px;
2422
+ font-size: 11px;
2423
+ }
2424
+
2425
+ .stepper-header::before {
2426
+ top: 14px;
2427
+ }
2428
+
2429
+ .step-content h2,
2430
+ .form-section h2 {
2431
+ font-size: 16px;
2432
+ }
2433
+
2434
+ .navigation-footer button {
2435
+ font-size: 13px;
2436
+ }
2437
+ }
2438
+ </style>
2439
+ `;
2440
+ }
2441
+
2442
+ renderStepperHeader() {
2443
+ return `
2444
+ <div class="stepper-header">
2445
+ ${this.STEPS.map((step, index) =>
2446
+ this.renderStepIndicator(step, index)
2447
+ ).join("")}
2448
+ </div>
2449
+ `;
2450
+ }
2451
+
2452
+ renderStepIndicator(step, index) {
2453
+ const isComplete = this.state.completedSteps.has(index);
2454
+ const isCurrent = this.state.currentStep === index;
2455
+ const isClickable = isComplete || index < this.state.currentStep;
2456
+
2457
+ return `
2458
+ <div class="step-indicator ${isCurrent ? "active" : ""} ${isComplete ? "complete" : ""
2459
+ } ${isClickable ? "clickable" : ""}"
2460
+ ${isClickable ? `data-step="${index}"` : ""}>
2461
+ <div class="step-circle">
2462
+ ${isComplete ? "✓" : index + 1}
2463
+ </div>
2464
+ <div class="step-label">${step.title}</div>
2465
+ </div>
2466
+ `;
2467
+ }
2468
+
2469
+ renderCurrentStep() {
2470
+ // This method is kept for backwards compatibility
2471
+ // but the modal now uses renderFormContent() instead
2472
+ return this.renderFormContent();
2473
+ }
2474
+
2475
+ renderUnderwritingStep() {
2476
+ return this.renderUnderwritingForm();
2477
+ }
2478
+
2479
+ renderUnderwritingForm() {
2480
+ const data = this.state.formData.underwriting;
2481
+ const underwritingDocuments = data.underwritingDocuments || [];
2482
+ const error = this.getFieldError("underwritingDocuments");
2483
+ const showErrors = this.state.uiState.showErrors;
2484
+
2485
+ return `
2486
+ <div class="form-section">
2487
+ <h2>Underwriting Documents</h2>
2488
+ <p>Upload supporting documents (required, max 10 files, 10MB each)</p>
2489
+
2490
+ <div class="form-grid">
2491
+ <div class="form-field full-width ${showErrors && error ? "has-error" : ""
2492
+ }">
2493
+ <label for="underwritingDocs">
2494
+ Upload Documents <span class="required-asterisk">*</span>
2495
+ <span style="font-size: 12px; color: var(--gray-medium); font-weight: normal;">
2496
+ (PDF, JPG, PNG, DOC, DOCX - Max 10MB each)
2497
+ </span>
2498
+ </label>
2499
+
2500
+ <div class="drag-drop-area" id="dragDropArea">
2501
+ <div class="drag-drop-content">
2502
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin: 0 auto var(--spacing-sm); display: block; color: var(--gray-medium);">
2503
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
2504
+ <polyline points="17 8 12 3 7 8"></polyline>
2505
+ <line x1="12" y1="3" x2="12" y2="15"></line>
2506
+ </svg>
2507
+ <p style="margin-bottom: var(--spacing-sm); color: var(--color-headline, #0f2a39); font-weight: 500;">
2508
+ Drag and drop files here
2509
+ </p>
2510
+ <p style="font-size: 14px; color: var(--gray-medium); margin-bottom: var(--spacing-md);">
2511
+ or
2512
+ </p>
2513
+ <button type="button" class="btn-browse" style="
2514
+ padding: 10px 20px;
2515
+ background: var(--primary-color);
2516
+ color: var(--color-white, #fff);
2517
+ border: none;
2518
+ border-radius: var(--border-radius-sm);
2519
+ cursor: pointer;
2520
+ font-size: 14px;
2521
+ font-weight: 500;
2522
+ ">Browse Files</button>
2523
+ <input
2524
+ type="file"
2525
+ id="underwritingDocs"
2526
+ name="underwritingDocs"
2527
+ multiple
2528
+ accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
2529
+ style="display: none;"
2530
+ />
2531
+ </div>
2532
+ </div>
2533
+
2534
+ <div id="fileList" style="margin-top: var(--spacing-md);">
2535
+ ${underwritingDocuments.length > 0
2536
+ ? this.renderFileList(underwritingDocuments)
2537
+ : ""
2538
+ }
2539
+ </div>
2540
+
2541
+ ${showErrors && error
2542
+ ? `<span class="error-message">${error}</span>`
2543
+ : ""
2544
+ }
2545
+ </div>
2546
+ </div>
2547
+ </div>
2548
+ `;
2549
+ }
2550
+
2551
+ renderBusinessDetailsStep() {
2552
+ return this.renderBusinessDetailsForm();
2553
+ }
2554
+
2555
+ renderFileList(files) {
2556
+ return `
2557
+ <div class="uploaded-files">
2558
+ <p style="font-size: 14px; font-weight: 500; margin-bottom: var(--spacing-sm); color: var(--color-headline, #0f2a39);">
2559
+ ${files.length} file(s) uploaded:
2560
+ </p>
2561
+ ${files
2562
+ .map(
2563
+ (file, index) => `
2564
+ <div class="file-item" data-index="${index}" style="
2565
+ display: flex;
2566
+ align-items: center;
2567
+ justify-content: space-between;
2568
+ padding: var(--spacing-sm);
2569
+ background: var(--gray-light);
2570
+ border-radius: var(--border-radius-sm);
2571
+ margin-bottom: var(--spacing-sm);
2572
+ ">
2573
+ <div style="display: flex; align-items: center; gap: var(--spacing-sm); flex: 1; min-width: 0;">
2574
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
2575
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
2576
+ <polyline points="13 2 13 9 20 9"></polyline>
2577
+ </svg>
2578
+ <span style="font-size: 14px; color: var(--color-headline, #0f2a39); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
2579
+ ${file.name}
2580
+ </span>
2581
+ <span style="font-size: 12px; color: var(--gray-medium); white-space: nowrap;">
2582
+ (${(file.size / 1024).toFixed(1)} KB)
2583
+ </span>
2584
+ </div>
2585
+ <button type="button" class="btn-remove-file" data-index="${index}" style="
2586
+ background: none;
2587
+ border: none;
2588
+ color: var(--error-color);
2589
+ cursor: pointer;
2590
+ padding: var(--spacing-sm);
2591
+ font-size: 14px;
2592
+ flex-shrink: 0;
2593
+ ">✕</button>
2594
+ </div>
2595
+ `
2596
+ )
2597
+ .join("")}
2598
+ </div>
2599
+ `;
2600
+ }
2601
+
2602
+ renderBusinessDetailsForm() {
2603
+ const data = this.state.formData.businessDetails;
2604
+
2605
+ return `
2606
+ <div class="form-section">
2607
+ <h2>Business Information</h2>
2608
+ <p>Provide your business details</p>
2609
+
2610
+ <div class="form-grid">
2611
+ ${this.renderField({
2612
+ name: "businessName",
2613
+ label: "Business Name *",
2614
+ value: data.businessName,
2615
+ error: this.getFieldError("businessName"),
2616
+ })}
2617
+
2618
+ ${this.renderField({
2619
+ name: "doingBusinessAs",
2620
+ label: "Doing Business As (DBA) *",
2621
+ value: data.doingBusinessAs,
2622
+ error: this.getFieldError("doingBusinessAs"),
2623
+ })}
2624
+
2625
+ ${this.renderField({
2626
+ name: "ein",
2627
+ label: "EIN *",
2628
+ value: data.ein,
2629
+ error: this.getFieldError("ein"),
2630
+ placeholder: "12-3456789",
2631
+ maxLength: 10,
2632
+ dataFormat: "ein",
2633
+ className: "full-width",
2634
+ })}
2635
+
2636
+ ${this.renderField({
2637
+ name: "businessWebsite",
2638
+ label: "Business Website *",
2639
+ type: "url",
2640
+ value: data.businessWebsite,
2641
+ error: this.getFieldError("businessWebsite"),
2642
+ placeholder: "https://example.com",
2643
+ className: "full-width",
2644
+ })}
2645
+
2646
+ ${this.renderField({
2647
+ name: "businessPhoneNumber",
2648
+ label: "Business Phone *",
2649
+ type: "tel",
2650
+ value: data.businessPhoneNumber,
2651
+ error: this.getFieldError("businessPhoneNumber"),
2652
+ placeholder: "(555) 123-4567",
2653
+ dataFormat: "phone",
2654
+ })}
2655
+
2656
+ ${this.renderField({
2657
+ name: "businessEmail",
2658
+ label: "Business Email *",
2659
+ type: "email",
2660
+ value: data.businessEmail,
2661
+ error: this.getFieldError("businessEmail"),
2662
+ readOnly: false,
2663
+ })}
2664
+
2665
+ ${this.renderField({
2666
+ name: "BusinessAddress1",
2667
+ label: "Street Address *",
2668
+ value: data.BusinessAddress1,
2669
+ error: this.getFieldError("BusinessAddress1"),
2670
+ className: "full-width",
2671
+ })}
2672
+
2673
+ ${this.renderField({
2674
+ name: "businessCity",
2675
+ label: "City *",
2676
+ value: data.businessCity,
2677
+ error: this.getFieldError("businessCity"),
2678
+ })}
2679
+
2680
+ <div class="form-field ${this.getFieldError("businessState") ? "has-error" : ""
2681
+ }">
2682
+ <label for="businessState">State <span class="required-asterisk">*</span></label>
2683
+ <select id="businessState" name="businessState">
2684
+ <option value="">Select State</option>
2685
+ ${this.US_STATES.map(
2686
+ (state) => `
2687
+ <option value="${state}" ${data.businessState === state ? "selected" : ""
2688
+ }>${state}</option>
2689
+ `
2690
+ ).join("")}
2691
+ </select>
2692
+ ${this.getFieldError("businessState")
2693
+ ? `<span class="error-message">${this.getFieldError(
2694
+ "businessState"
2695
+ )}</span>`
2696
+ : ""
2697
+ }
2698
+ </div>
2699
+
2700
+ ${this.renderField({
2701
+ name: "businessPostalCode",
2702
+ label: "ZIP Code *",
2703
+ value: data.businessPostalCode,
2704
+ error: this.getFieldError("businessPostalCode"),
2705
+ placeholder: "12345",
2706
+ maxLength: 5,
2707
+ })}
2708
+ </div>
2709
+ </div>
2710
+ `;
2711
+ }
2712
+
2713
+ renderRepresentativesStep() {
2714
+ return this.renderRepresentativesForm();
2715
+ }
2716
+
2717
+ renderRepresentativesForm() {
2718
+ const representatives = this.state.formData.representatives;
2719
+
2720
+ return `
2721
+ <div class="form-section">
2722
+ <h2>Business Representatives</h2>
2723
+ <p>Add business representatives (optional)</p>
2724
+
2725
+ <div class="representatives-list">
2726
+ ${representatives.length === 0
2727
+ ? `
2728
+ <div class="empty-state">
2729
+ <p>No representatives added yet. Click below to add one.</p>
2730
+ </div>
2731
+ `
2732
+ : ""
2733
+ }
2734
+ ${representatives
2735
+ .map((rep, index) => this.renderRepresentativeCard(rep, index))
2736
+ .join("")}
2737
+ </div>
2738
+
2739
+ <button type="button" class="add-representative-btn">
2740
+ + Add Representative
2741
+ </button>
2742
+ </div>
2743
+ `;
2744
+ }
2745
+
2746
+ renderRepresentativeCard(representative, index) {
2747
+ return `
2748
+ <div class="representative-card" data-index="${index}">
2749
+ <div class="card-header">
2750
+ <h3>Representative ${index + 1}</h3>
2751
+ <button type="button" class="remove-btn" data-index="${index}">Remove</button>
2752
+ </div>
2753
+ <div class="card-body">
2754
+ <div class="form-grid">
2755
+ ${this.renderField({
2756
+ name: "representativeFirstName",
2757
+ label: "First Name *",
2758
+ value: representative.representativeFirstName,
2759
+ error: this.getFieldError("representativeFirstName", index),
2760
+ dataRepIndex: index,
2761
+ })}
2762
+
2763
+ ${this.renderField({
2764
+ name: "representativeLastName",
2765
+ label: "Last Name *",
2766
+ value: representative.representativeLastName,
2767
+ error: this.getFieldError("representativeLastName", index),
2768
+ dataRepIndex: index,
2769
+ })}
2770
+
2771
+ ${this.renderField({
2772
+ name: "representativeJobTitle",
2773
+ label: "Job Title *",
2774
+ value: representative.representativeJobTitle,
2775
+ error: this.getFieldError("representativeJobTitle", index),
2776
+ dataRepIndex: index,
2777
+ className: "full-width",
2778
+ })}
2779
+
2780
+ ${this.renderField({
2781
+ name: "representativePhone",
2782
+ label: "Phone *",
2783
+ type: "tel",
2784
+ value: representative.representativePhone,
2785
+ error: this.getFieldError("representativePhone", index),
2786
+ placeholder: "(555) 123-4567",
2787
+ dataRepIndex: index,
2788
+ dataFormat: "phone",
2789
+ })}
2790
+
2791
+ ${this.renderField({
2792
+ name: "representativeEmail",
2793
+ label: "Email *",
2794
+ type: "email",
2795
+ value: representative.representativeEmail,
2796
+ error: this.getFieldError("representativeEmail", index),
2797
+ dataRepIndex: index,
2798
+ })}
2799
+
2800
+ ${this.renderField({
2801
+ name: "representativeDateOfBirth",
2802
+ label: "Date of Birth *",
2803
+ type: "date",
2804
+ value: representative.representativeDateOfBirth,
2805
+ error: this.getFieldError("representativeDateOfBirth", index),
2806
+ dataRepIndex: index,
2807
+ className: "full-width",
2808
+ })}
2809
+
2810
+ ${this.renderField({
2811
+ name: "representativeAddress",
2812
+ label: "Address *",
2813
+ value: representative.representativeAddress,
2814
+ error: this.getFieldError("representativeAddress", index),
2815
+ dataRepIndex: index,
2816
+ className: "full-width",
2817
+ })}
2818
+
2819
+ ${this.renderField({
2820
+ name: "representativeCity",
2821
+ label: "City *",
2822
+ value: representative.representativeCity,
2823
+ error: this.getFieldError("representativeCity", index),
2824
+ dataRepIndex: index,
2825
+ })}
2826
+
2827
+ <div class="form-field ${this.getFieldError("representativeState", index)
2828
+ ? "has-error"
2829
+ : ""
2830
+ }">
2831
+ <label for="representativeState-${index}">State <span class="required-asterisk">*</span></label>
2832
+ <select id="representativeState-${index}" name="representativeState" data-rep-index="${index}">
2833
+ <option value="">Select State</option>
2834
+ ${this.US_STATES.map(
2835
+ (state) => `
2836
+ <option value="${state}" ${representative.representativeState === state
2837
+ ? "selected"
2838
+ : ""
2839
+ }>${state}</option>
2840
+ `
2841
+ ).join("")}
2842
+ </select>
2843
+ ${this.getFieldError("representativeState", index)
2844
+ ? `<span class="error-message">${this.getFieldError(
2845
+ "representativeState",
2846
+ index
2847
+ )}</span>`
2848
+ : ""
2849
+ }
2850
+ </div>
2851
+
2852
+ ${this.renderField({
2853
+ name: "representativeZip",
2854
+ label: "ZIP Code *",
2855
+ value: representative.representativeZip,
2856
+ error: this.getFieldError("representativeZip", index),
2857
+ placeholder: "12345",
2858
+ maxLength: 5,
2859
+ dataRepIndex: index,
2860
+ })}
2861
+ </div>
2862
+ </div>
2863
+ </div>
2864
+ `;
2865
+ }
2866
+
2867
+ renderBankDetailsStep() {
2868
+ return this.renderBankDetailsForm();
2869
+ }
2870
+
2871
+ renderBankDetailsForm() {
2872
+ const data = this.state.formData.bankDetails;
2873
+
2874
+ return `
2875
+ <div class="form-section">
2876
+ <h2>Bank Account</h2>
2877
+ <p>Link your bank account</p>
2878
+
2879
+ <div class="form-grid">
2880
+ ${this.renderField({
2881
+ name: "bankAccountHolderName",
2882
+ label: "Account Holder Name *",
2883
+ value: data.bankAccountHolderName,
2884
+ error: this.getFieldError("bankAccountHolderName"),
2885
+ className: "full-width",
2886
+ })}
2887
+
2888
+ <div class="form-field full-width">
2889
+ <label>Account Type <span class="required-asterisk">*</span></label>
2890
+ <div class="radio-group">
2891
+ <div class="radio-option">
2892
+ <input type="radio" id="checking" name="bankAccountType" value="checking" ${data.bankAccountType === "checking" ? "checked" : ""
2893
+ }>
2894
+ <label for="checking">Checking</label>
2895
+ </div>
2896
+ <div class="radio-option">
2897
+ <input type="radio" id="savings" name="bankAccountType" value="savings" ${data.bankAccountType === "savings" ? "checked" : ""
2898
+ }>
2899
+ <label for="savings">Savings</label>
2900
+ </div>
2901
+ </div>
2902
+ </div>
2903
+
2904
+ ${this.renderField({
2905
+ name: "bankRoutingNumber",
2906
+ label: "Routing Number *",
2907
+ value: data.bankRoutingNumber,
2908
+ error: this.getFieldError("bankRoutingNumber"),
2909
+ placeholder: "123456789",
2910
+ maxLength: 9,
2911
+ })}
2912
+
2913
+ ${this.renderField({
2914
+ name: "bankAccountNumber",
2915
+ label: "Account Number *",
2916
+ value: data.bankAccountNumber,
2917
+ error: this.getFieldError("bankAccountNumber"),
2918
+ placeholder: "1234567890",
2919
+ })}
2920
+ </div>
2921
+ </div>
2922
+ `;
2923
+ }
2924
+
2925
+ renderField({
2926
+ name,
2927
+ label,
2928
+ type = "text",
2929
+ value = "",
2930
+ error = "",
2931
+ readOnly = false,
2932
+ placeholder = "",
2933
+ className = "",
2934
+ maxLength = null,
2935
+ dataRepIndex = null,
2936
+ dataFormat = null,
2937
+ }) {
2938
+ const fieldClass = `form-field ${error ? "has-error" : ""} ${className}`;
2939
+ const fieldId = dataRepIndex !== null ? `${name}-${dataRepIndex}` : name;
2940
+
2941
+ return `
2942
+ <div class="${fieldClass}">
2943
+ <label for="${fieldId}">${label.replace(
2944
+ " *",
2945
+ ' <span class="required-asterisk">*</span>'
2946
+ )}</label>
2947
+ <input
2948
+ type="${type}"
2949
+ id="${fieldId}"
2950
+ name="${name}"
2951
+ value="${value}"
2952
+ ${readOnly ? "readonly" : ""}
2953
+ ${placeholder ? `placeholder="${placeholder}"` : ""}
2954
+ ${maxLength ? `maxlength="${maxLength}"` : ""}
2955
+ ${dataRepIndex !== null ? `data-rep-index="${dataRepIndex}"` : ""}
2956
+ ${dataFormat ? `data-format="${dataFormat}"` : ""}
2957
+ />
2958
+ ${error ? `<span class="error-message">${error}</span>` : ""}
2959
+ </div>
2960
+ `;
2961
+ }
2962
+
2963
+ renderNavigationFooter() {
2964
+ const isFirstStep = this.state.currentStep === 0;
2965
+ const isLastStep = this.state.currentStep === this.state.totalSteps - 1;
2966
+ const canSkip = this.STEPS[this.state.currentStep].canSkip;
2967
+ console.log("[FORM DATA]: ", this.state.formData);
2968
+
2969
+ // Hide back button on first step (Business Details)
2970
+ const showBack = !isFirstStep;
2971
+
2972
+ return `
2973
+ <div class="navigation-footer">
2974
+ ${showBack ? '<button type="button" class="btn-back">Back</button>' : ""
2975
+ }
2976
+ ${canSkip ? '<button type="button" class="btn-skip">Skip</button>' : ""}
2977
+ <button type="button" class="btn-next">
2978
+ ${isLastStep ? "Submit" : "Next"}
2979
+ </button>
2980
+ </div>
2981
+ `;
2982
+ }
2983
+
2984
+ renderSuccessPage() {
2985
+ const { businessDetails, representatives, bankDetails } =
2986
+ this.state.formData;
2987
+
2988
+ return `
2989
+ <div class="success-container">
2990
+ <div class="success-icon">
2991
+ <svg viewBox="0 0 52 52">
2992
+ <path d="M14 27l7 7 16-16"/>
2993
+ </svg>
2994
+ </div>
2995
+
2996
+ <h2>Onboarding Complete! 🎉</h2>
2997
+ <p>Your operator application has been successfully submitted.</p>
2998
+ <p style="margin-top: var(--spacing-lg); color: var(--color-headline, #0f2a39);">
2999
+ <strong>You can now close this dialog.</strong>
3000
+ </p>
3001
+
3002
+ <div class="success-details">
3003
+ <h3>Submission Summary</h3>
3004
+ <div class="detail-item">
3005
+ <span class="detail-label">Business Name</span>
3006
+ <span class="detail-value">${businessDetails.businessName}</span>
3007
+ </div>
3008
+ <div class="detail-item">
3009
+ <span class="detail-label">Business Email</span>
3010
+ <span class="detail-value">${businessDetails.businessEmail}</span>
3011
+ </div>
3012
+ <div class="detail-item">
3013
+ <span class="detail-label">Phone Number</span>
3014
+ <span class="detail-value">${businessDetails.businessPhoneNumber
3015
+ }</span>
3016
+ </div>
3017
+ <div class="detail-item">
3018
+ <span class="detail-label">Representatives</span>
3019
+ <span class="detail-value">${representatives.length} added</span>
3020
+ </div>
3021
+ <div class="detail-item">
3022
+ <span class="detail-label">Bank Account</span>
3023
+ <span class="detail-value">${bankDetails.bankAccountType === "checking"
3024
+ ? "Checking"
3025
+ : "Savings"
3026
+ } (****${bankDetails.bankAccountNumber.slice(-4)})</span>
3027
+ </div>
3028
+ </div>
3029
+
3030
+ <p style="font-size: 14px; color: var(--gray-medium); margin-top: var(--spacing-lg);">
3031
+ A confirmation email has been sent to <strong>${businessDetails.businessEmail
3032
+ }</strong>
3033
+ </p>
3034
+
3035
+ <button class="btn-confirm-success" type="button">
3036
+ Done
3037
+ </button>
3038
+ </div>
3039
+ `;
3040
+ }
3041
+
3042
+ renderSubmissionFailurePage() {
3043
+ const { errorMessage } = this.state.uiState;
3044
+
3045
+ return `
3046
+ <div class="error-container">
3047
+ <div class="error-icon">
3048
+ <svg viewBox="0 0 52 52">
3049
+ <circle cx="26" cy="26" r="25" fill="none"/>
3050
+ <path d="M16 16 L36 36 M36 16 L16 36"/>
3051
+ </svg>
3052
+ </div>
3053
+
3054
+ <h2>Submission Failed</h2>
3055
+ <p>Your onboarding submission could not be processed.</p>
3056
+
3057
+ <div class="error-details">
3058
+ <h3>Error Details</h3>
3059
+ <p><strong>Issue:</strong> ${errorMessage || "The submission failed due to a server error."
3060
+ }</p>
3061
+ <p style="margin-top: var(--spacing-md);">
3062
+ Please try submitting again. If the problem persists, contact support.
3063
+ </p>
3064
+ </div>
3065
+
3066
+ <div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-sm); justify-content: center;">
3067
+ <button type="button" class="btn-resubmit" style="
3068
+ padding: 12px 24px;
3069
+ background: var(--primary-color);
3070
+ color: var(--color-white, #fff);
3071
+ border: none;
3072
+ border-radius: var(--border-radius);
3073
+ font-size: 14px;
3074
+ font-weight: 500;
3075
+ cursor: pointer;
3076
+ ">Resubmit</button>
3077
+ </div>
3078
+
3079
+ <p style="margin-top: var(--spacing-md); font-size: 12px; color: var(--gray-medium);">
3080
+ Or you can close this dialog.
3081
+ </p>
3082
+ </div>
3083
+ `;
3084
+ }
3085
+
3086
+ // ==================== EVENT HANDLING ====================
3087
+
3088
+ attachSubmissionFailureListeners() {
3089
+ const shadow = this.shadowRoot;
3090
+
3091
+ // Resubmit button (on submission failure page)
3092
+ const resubmitBtn = shadow.querySelector(".btn-resubmit");
3093
+ if (resubmitBtn) {
3094
+ resubmitBtn.addEventListener("mousedown", async (e) => {
3095
+ e.preventDefault(); // Prevent blur from interfering
3096
+ // Call onError callback with resubmit action
3097
+ if (this.onError && typeof this.onError === "function") {
3098
+ this.onError({ action: "resubmit", formData: this.state.formData });
3099
+ }
3100
+
3101
+ // For now, just reset to last step
3102
+ // TODO: Implement actual resubmission logic
3103
+ this.setState({
3104
+ isSubmissionFailed: false,
3105
+ currentStep: this.state.totalSteps - 1,
3106
+ uiState: { showErrors: false, errorMessage: null },
3107
+ });
3108
+ });
3109
+ }
3110
+ }
3111
+
3112
+ attachFailurePageListeners() {
3113
+ const shadow = this.shadowRoot;
3114
+ // This method is currently unused but kept for future error handling
3115
+ }
3116
+
3117
+ attachEventListeners() {
3118
+ const shadow = this.shadowRoot;
3119
+
3120
+ // Trigger button to open modal
3121
+ const triggerBtn = shadow.querySelector(".onboarding-trigger-btn");
3122
+ if (triggerBtn) {
3123
+ triggerBtn.addEventListener("click", () => this.openModal());
3124
+ }
3125
+
3126
+ // Modal close button
3127
+ const closeBtn = shadow.querySelector(".modal-close-btn");
3128
+ if (closeBtn) {
3129
+ closeBtn.addEventListener("click", () => this.closeModal());
3130
+ }
3131
+
3132
+ // Close on overlay click (outside modal) - stop event from bubbling from modal-container
3133
+ // Prevent closing during submission and on success screen (require confirm button)
3134
+ const overlay = shadow.querySelector(".modal-overlay");
3135
+ const modalContainer = shadow.querySelector(".modal-container");
3136
+ if (overlay) {
3137
+ overlay.addEventListener("click", (e) => {
3138
+ // Prevent closing during submission or on success screen
3139
+ if (this.state.uiState.isLoading || this.state.isSubmitted) {
3140
+ return;
3141
+ }
3142
+ // Only close if clicking exactly on the overlay, not inside the modal
3143
+ if (e.target === overlay) {
3144
+ this.closeModal();
3145
+ }
3146
+ });
3147
+
3148
+ // Prevent clicks inside modal container from bubbling to overlay
3149
+ if (modalContainer) {
3150
+ modalContainer.addEventListener("click", (e) => {
3151
+ e.stopPropagation();
3152
+ });
3153
+ }
3154
+ }
3155
+
3156
+ // Close on Escape key (prevent during submission and on success screen)
3157
+ if (this.state.isModalOpen) {
3158
+ this._escapeHandler = (e) => {
3159
+ // Prevent closing during submission or on success screen
3160
+ if (this.state.uiState.isLoading || this.state.isSubmitted) {
3161
+ return;
3162
+ }
3163
+ if (e.key === "Escape") {
3164
+ this.closeModal();
3165
+ }
3166
+ };
3167
+ document.addEventListener("keydown", this._escapeHandler);
3168
+ }
3169
+
3170
+ // Attach submission failure listeners if needed
3171
+ if (this.state.isSubmissionFailed) {
3172
+ this.attachSubmissionFailureListeners();
3173
+ }
3174
+
3175
+ // Success confirmation button
3176
+ const confirmBtn = shadow.querySelector(".btn-confirm-success");
3177
+ if (confirmBtn) {
3178
+ confirmBtn.addEventListener("click", () => this.handleSuccessConfirm());
3179
+ }
3180
+
3181
+ // Form inputs - blur validation (only when modal is open)
3182
+ if (this.state.isModalOpen) {
3183
+ shadow.querySelectorAll("input, select").forEach((input) => {
3184
+ input.addEventListener("blur", (e) => this.handleFieldBlur(e));
3185
+ input.addEventListener("input", (e) => this.handleFieldInput(e));
3186
+ });
3187
+ }
3188
+
3189
+ // Navigation buttons - use mousedown to prevent blur interference
3190
+ const nextBtn = shadow.querySelector(".btn-next");
3191
+ if (nextBtn) {
3192
+ nextBtn.addEventListener("mousedown", (e) => {
3193
+ e.preventDefault(); // Prevent blur from interfering
3194
+ this.goToNextStep();
3195
+ });
3196
+ }
3197
+
3198
+ const backBtn = shadow.querySelector(".btn-back");
3199
+ if (backBtn) {
3200
+ backBtn.addEventListener("mousedown", (e) => {
3201
+ e.preventDefault(); // Prevent blur from interfering
3202
+ this.goToPreviousStep();
3203
+ });
3204
+ }
3205
+
3206
+ const skipBtn = shadow.querySelector(".btn-skip");
3207
+ if (skipBtn) {
3208
+ skipBtn.addEventListener("mousedown", (e) => {
3209
+ e.preventDefault(); // Prevent blur from interfering
3210
+ this.skipStep();
3211
+ });
3212
+ }
3213
+
3214
+ // Step indicators (for navigation)
3215
+ shadow.querySelectorAll("[data-step]").forEach((indicator) => {
3216
+ indicator.addEventListener("click", (e) => {
3217
+ const stepIndex = parseInt(e.currentTarget.dataset.step);
3218
+ this.goToStep(stepIndex);
3219
+ });
3220
+ });
3221
+
3222
+ // Representative CRUD - use mousedown to prevent blur interference
3223
+ const addBtn = shadow.querySelector(".add-representative-btn");
3224
+ if (addBtn) {
3225
+ addBtn.addEventListener("mousedown", (e) => {
3226
+ e.preventDefault(); // Prevent blur from interfering
3227
+ this.addRepresentative();
3228
+ });
3229
+ }
3230
+
3231
+ shadow.querySelectorAll(".remove-btn").forEach((btn) => {
3232
+ btn.addEventListener("mousedown", (e) => {
3233
+ e.preventDefault(); // Prevent blur from interfering
3234
+ const index = parseInt(e.target.dataset.index);
3235
+ this.removeRepresentative(index);
3236
+ });
3237
+ });
3238
+
3239
+ // File upload handlers for underwriting documents
3240
+ const fileInput = shadow.querySelector("#underwritingDocs");
3241
+ const dragDropArea = shadow.querySelector("#dragDropArea");
3242
+ const browseBtn = shadow.querySelector(".btn-browse");
3243
+
3244
+ if (fileInput && dragDropArea) {
3245
+ // Browse button click
3246
+ if (browseBtn) {
3247
+ browseBtn.addEventListener("click", (e) => {
3248
+ e.stopPropagation();
3249
+ fileInput.click();
3250
+ });
3251
+ }
3252
+
3253
+ // Click on drag area
3254
+ dragDropArea.addEventListener("click", () => {
3255
+ fileInput.click();
3256
+ });
3257
+
3258
+ // Drag and drop events
3259
+ dragDropArea.addEventListener("dragenter", (e) => {
3260
+ e.preventDefault();
3261
+ e.stopPropagation();
3262
+ dragDropArea.classList.add("drag-over");
3263
+ });
3264
+
3265
+ dragDropArea.addEventListener("dragover", (e) => {
3266
+ e.preventDefault();
3267
+ e.stopPropagation();
3268
+ dragDropArea.classList.add("drag-over");
3269
+ });
3270
+
3271
+ dragDropArea.addEventListener("dragleave", (e) => {
3272
+ e.preventDefault();
3273
+ e.stopPropagation();
3274
+ if (e.target === dragDropArea) {
3275
+ dragDropArea.classList.remove("drag-over");
3276
+ }
3277
+ });
3278
+
3279
+ dragDropArea.addEventListener("drop", (e) => {
3280
+ e.preventDefault();
3281
+ e.stopPropagation();
3282
+ dragDropArea.classList.remove("drag-over");
3283
+
3284
+ const files = Array.from(e.dataTransfer.files);
3285
+ this.handleFileUpload(files);
3286
+ });
3287
+
3288
+ // File input change
3289
+ fileInput.addEventListener("change", (e) => {
3290
+ const files = Array.from(e.target.files);
3291
+ this.handleFileUpload(files);
3292
+ });
3293
+ }
3294
+
3295
+ // Remove file buttons
3296
+ shadow.querySelectorAll(".btn-remove-file").forEach((btn) => {
3297
+ btn.addEventListener("click", (e) => {
3298
+ e.stopPropagation();
3299
+ const index = parseInt(btn.dataset.index);
3300
+ this.removeFile(index);
3301
+ });
3302
+ });
3303
+ }
3304
+
3305
+ handleFileUpload(files) {
3306
+ // Validate files
3307
+ const errors = [];
3308
+ const validFiles = [];
3309
+ const maxSize = 10 * 1024 * 1024; // 10MB
3310
+ const maxFiles = 10;
3311
+ const allowedTypes = [".pdf", ".jpg", ".jpeg", ".png", ".doc", ".docx"];
3312
+
3313
+ // Get existing documents
3314
+ const existingDocs =
3315
+ this.state.formData.underwriting.underwritingDocuments || [];
3316
+ const totalFiles = existingDocs.length + files.length;
3317
+
3318
+ if (totalFiles > maxFiles) {
3319
+ errors.push(
3320
+ `Maximum ${maxFiles} files allowed (you have ${existingDocs.length} already)`
3321
+ );
3322
+ }
3323
+
3324
+ files.forEach((file) => {
3325
+ // Check file size
3326
+ if (file.size > maxSize) {
3327
+ errors.push(`${file.name} exceeds 10MB limit`);
3328
+ } else if (file.size === 0) {
3329
+ errors.push(`${file.name} is empty`);
3330
+ } else {
3331
+ // Check file type
3332
+ const ext = "." + file.name.split(".").pop().toLowerCase();
3333
+ if (allowedTypes.includes(ext)) {
3334
+ validFiles.push(file);
3335
+ } else {
3336
+ errors.push(`${file.name} is not an allowed file type`);
3337
+ }
3338
+ }
3339
+ });
3340
+
3341
+ // Combine with existing documents
3342
+ const allDocs = [...existingDocs, ...validFiles].slice(0, maxFiles);
3343
+
3344
+ console.log("[FILE UPLOAD ERRORS]: ", errors);
3345
+
3346
+ // Update state with valid files
3347
+ this.setState({
3348
+ formData: {
3349
+ underwriting: {
3350
+ ...this.state.formData.underwriting,
3351
+ underwritingDocuments: allDocs,
3352
+ },
3353
+ },
3354
+ uiState: {
3355
+ errorMessage: errors.length > 0 ? errors.join("; ") : null,
3356
+ },
3357
+ });
3358
+
3359
+ // Show errors if any
3360
+ if (errors.length > 0) {
3361
+ const fileList = this.shadowRoot.querySelector("#fileList");
3362
+ if (fileList) {
3363
+ const errorDiv = document.createElement("div");
3364
+ errorDiv.style.color = "var(--error-color)";
3365
+ errorDiv.style.fontSize = "12px";
3366
+ errorDiv.style.marginTop = "var(--spacing-sm)";
3367
+ errorDiv.textContent = errors.join("; ");
3368
+ fileList.prepend(errorDiv);
3369
+
3370
+ // Remove error message after 5 seconds
3371
+ setTimeout(() => errorDiv.remove(), 5000);
3372
+ }
3373
+ }
3374
+ }
3375
+
3376
+ removeFile(index) {
3377
+ const underwritingDocuments = [
3378
+ ...this.state.formData.underwriting.underwritingDocuments,
3379
+ ];
3380
+
3381
+ underwritingDocuments.splice(index, 1);
3382
+
3383
+ this.setState({
3384
+ formData: {
3385
+ underwriting: {
3386
+ ...this.state.formData.underwriting,
3387
+ underwritingDocuments,
3388
+ },
3389
+ },
3390
+ });
3391
+ }
3392
+
3393
+ handleFieldBlur(e) {
3394
+ const input = e.target;
3395
+ const name = input.name;
3396
+ const value = input.value;
3397
+ const repIndex = input.dataset.repIndex;
3398
+
3399
+ // Phone number formatting on blur
3400
+ if (input.dataset.format === "phone") {
3401
+ input.value = this.formatPhoneNumber(value);
3402
+ }
3403
+
3404
+ // EIN formatting on blur
3405
+ if (input.dataset.format === "ein") {
3406
+ input.value = this.formatEIN(value);
3407
+ }
3408
+
3409
+ // URL normalization on blur
3410
+ if (input.type === "url" && value) {
3411
+ const validationResult = this.validators.url(value);
3412
+ if (validationResult.isValid && validationResult.normalizedValue) {
3413
+ input.value = validationResult.normalizedValue;
3414
+ }
3415
+ }
3416
+
3417
+ // Update state based on step - directly modify state without re-rendering
3418
+ const stepId = this.STEPS[this.state.currentStep].id;
3419
+
3420
+ if (stepId === "business-details") {
3421
+ this.state.formData.businessDetails[name] = input.value;
3422
+ } else if (stepId === "representatives" && repIndex !== undefined) {
3423
+ const idx = parseInt(repIndex);
3424
+ if (this.state.formData.representatives[idx]) {
3425
+ this.state.formData.representatives[idx][name] = input.value;
3426
+ }
3427
+ } else if (stepId === "underwriting") {
3428
+ this.state.formData.underwriting[name] = input.value;
3429
+ } else if (stepId === "bank-details") {
3430
+ this.state.formData.bankDetails[name] = input.value;
3431
+ }
3432
+ }
3433
+
3434
+ handleFieldInput(e) {
3435
+ const input = e.target;
3436
+ const name = input.name;
3437
+ let value = input.value;
3438
+ const repIndex = input.dataset.repIndex;
3439
+
3440
+ // Apply real-time formatting for EIN
3441
+ if (input.dataset.format === "ein") {
3442
+ const cursorPosition = input.selectionStart;
3443
+ const oldValue = value;
3444
+ value = this.formatEIN(value);
3445
+ input.value = value;
3446
+
3447
+ // Adjust cursor position after formatting
3448
+ if (oldValue.length < value.length) {
3449
+ // If a hyphen was added, move cursor after it
3450
+ input.setSelectionRange(cursorPosition + 1, cursorPosition + 1);
3451
+ } else {
3452
+ input.setSelectionRange(cursorPosition, cursorPosition);
3453
+ }
3454
+ }
3455
+
3456
+ // Apply real-time formatting for phone numbers
3457
+ if (input.dataset.format === "phone") {
3458
+ const cursorPosition = input.selectionStart;
3459
+ const oldValue = value;
3460
+ value = this.formatPhoneNumber(value);
3461
+ input.value = value;
3462
+
3463
+ // Adjust cursor position after formatting
3464
+ const diff = value.length - oldValue.length;
3465
+ if (diff > 0) {
3466
+ // Characters were added (formatting), move cursor forward
3467
+ input.setSelectionRange(cursorPosition + diff, cursorPosition + diff);
3468
+ } else {
3469
+ input.setSelectionRange(cursorPosition, cursorPosition);
3470
+ }
3471
+ }
3472
+
3473
+ // Update state in real-time
3474
+ const stepId = this.STEPS[this.state.currentStep].id;
3475
+
3476
+ if (stepId === "business-details") {
3477
+ this.state.formData.businessDetails[name] = value;
3478
+ } else if (stepId === "representatives" && repIndex !== undefined) {
3479
+ const idx = parseInt(repIndex);
3480
+ if (this.state.formData.representatives[idx]) {
3481
+ this.state.formData.representatives[idx][name] = value;
3482
+ }
3483
+ } else if (stepId === "underwriting") {
3484
+ // TODO: Add underwriting field handling here when fields are added
3485
+ this.state.formData.underwriting[name] = value;
3486
+ } else if (stepId === "bank-details") {
3487
+ this.state.formData.bankDetails[name] = value;
3488
+ }
3489
+
3490
+ // Real-time validation: validate the field and update error state
3491
+ const stepKey = `step${this.state.currentStep}`;
3492
+
3493
+ // Initialize errors object if it doesn't exist
3494
+ if (!this.state.validationState[stepKey]) {
3495
+ this.state.validationState[stepKey] = { isValid: true, errors: {} };
3496
+ }
3497
+ if (!this.state.validationState[stepKey].errors) {
3498
+ this.state.validationState[stepKey].errors = {};
3499
+ }
3500
+
3501
+ // Only validate if showErrors is true (after first submit attempt)
3502
+ if (this.state.uiState.showErrors) {
3503
+ // Get field configuration for validation
3504
+ let validators = [];
3505
+ let fieldLabel = name;
3506
+
3507
+ if (stepId === "business-details") {
3508
+ const fieldConfigs = {
3509
+ businessName: { validators: ["required"], label: "Business Name" },
3510
+ doingBusinessAs: {
3511
+ validators: ["required"],
3512
+ label: "Doing Business As (DBA)",
3513
+ },
3514
+ ein: { validators: ["required", "ein"], label: "EIN" },
3515
+ businessWebsite: { validators: ["url"], label: "Business Website" },
3516
+ businessPhoneNumber: {
3517
+ validators: ["required", "usPhone"],
3518
+ label: "Business Phone",
3519
+ },
3520
+ businessEmail: {
3521
+ validators: ["required", "email"],
3522
+ label: "Business Email",
3523
+ },
3524
+ BusinessAddress1: {
3525
+ validators: ["required"],
3526
+ label: "Street Address",
3527
+ },
3528
+ businessCity: { validators: ["required"], label: "City" },
3529
+ businessState: { validators: ["required"], label: "State" },
3530
+ businessPostalCode: {
3531
+ validators: ["required", "postalCode"],
3532
+ label: "ZIP Code",
3533
+ },
3534
+ };
3535
+ if (fieldConfigs[name]) {
3536
+ validators = fieldConfigs[name].validators;
3537
+ fieldLabel = fieldConfigs[name].label;
3538
+ }
3539
+ } else if (stepId === "representatives" && repIndex !== undefined) {
3540
+ const fieldConfigs = {
3541
+ representativeFirstName: {
3542
+ validators: ["required"],
3543
+ label: "First Name",
3544
+ },
3545
+ representativeLastName: {
3546
+ validators: ["required"],
3547
+ label: "Last Name",
3548
+ },
3549
+ representativeJobTitle: {
3550
+ validators: ["required"],
3551
+ label: "Job Title",
3552
+ },
3553
+ representativePhone: {
3554
+ validators: ["required", "usPhone"],
3555
+ label: "Phone",
3556
+ },
3557
+ representativeEmail: {
3558
+ validators: ["required", "email"],
3559
+ label: "Email",
3560
+ },
3561
+ representativeDateOfBirth: {
3562
+ validators: ["required"],
3563
+ label: "Date of Birth",
3564
+ },
3565
+ representativeAddress: { validators: ["required"], label: "Address" },
3566
+ representativeCity: { validators: ["required"], label: "City" },
3567
+ representativeState: { validators: ["required"], label: "State" },
3568
+ representativeZip: {
3569
+ validators: ["required", "postalCode"],
3570
+ label: "ZIP Code",
3571
+ },
3572
+ };
3573
+ if (fieldConfigs[name]) {
3574
+ validators = fieldConfigs[name].validators;
3575
+ fieldLabel = fieldConfigs[name].label;
3576
+ }
3577
+ } else if (stepId === "underwriting") {
3578
+ // TODO: Add underwriting field validation configs here when fields are added
3579
+ // Example:
3580
+ // const fieldConfigs = {
3581
+ // industryType: { validators: ["required"], label: "Industry Type" },
3582
+ // };
3583
+ // if (fieldConfigs[name]) {
3584
+ // validators = fieldConfigs[name].validators;
3585
+ // fieldLabel = fieldConfigs[name].label;
3586
+ // }
3587
+ } else if (stepId === "bank-details") {
3588
+ const fieldConfigs = {
3589
+ accountHolderName: {
3590
+ validators: ["required"],
3591
+ label: "Account Holder Name",
3592
+ },
3593
+ accountType: { validators: ["required"], label: "Account Type" },
3594
+ routingNumber: {
3595
+ validators: ["required", "routingNumber"],
3596
+ label: "Routing Number",
3597
+ },
3598
+ accountNumber: {
3599
+ validators: ["required", "accountNumber"],
3600
+ label: "Account Number",
3601
+ },
3602
+ };
3603
+ if (fieldConfigs[name]) {
3604
+ validators = fieldConfigs[name].validators;
3605
+ fieldLabel = fieldConfigs[name].label;
3606
+ }
3607
+ }
3608
+
3609
+ // Validate the field
3610
+ if (validators.length > 0) {
3611
+ const error = this.validateField(value, validators, fieldLabel);
3612
+
3613
+ if (repIndex !== undefined) {
3614
+ // Handle representative field errors
3615
+ const repKey = `rep${repIndex}`;
3616
+ if (!this.state.validationState[stepKey].errors[repKey]) {
3617
+ this.state.validationState[stepKey].errors[repKey] = {};
3618
+ }
3619
+
3620
+ if (error) {
3621
+ this.state.validationState[stepKey].errors[repKey][name] = error;
3622
+ } else {
3623
+ delete this.state.validationState[stepKey].errors[repKey][name];
3624
+ // If no more errors for this rep, remove the rep key
3625
+ if (
3626
+ Object.keys(this.state.validationState[stepKey].errors[repKey])
3627
+ .length === 0
3628
+ ) {
3629
+ delete this.state.validationState[stepKey].errors[repKey];
3630
+ }
3631
+ }
3632
+ } else {
3633
+ // Handle regular field errors
3634
+ if (error) {
3635
+ this.state.validationState[stepKey].errors[name] = error;
3636
+ } else {
3637
+ delete this.state.validationState[stepKey].errors[name];
3638
+ }
3639
+ }
3640
+
3641
+ // Update error message in DOM without full re-render
3642
+ this.updateFieldErrorDisplay(input, error);
3643
+ }
3644
+ }
3645
+ }
3646
+
3647
+ updateFieldErrorDisplay(input, error) {
3648
+ // Find the parent form-field div
3649
+ const formField = input.closest(".form-field");
3650
+ if (!formField) return;
3651
+
3652
+ // Update has-error class
3653
+ if (error) {
3654
+ formField.classList.add("has-error");
3655
+ } else {
3656
+ formField.classList.remove("has-error");
3657
+ }
3658
+
3659
+ // Find or create error message element
3660
+ let errorSpan = formField.querySelector(".error-message");
3661
+
3662
+ if (error) {
3663
+ if (errorSpan) {
3664
+ // Update existing error message
3665
+ errorSpan.textContent = error;
3666
+ } else {
3667
+ // Create new error message
3668
+ errorSpan = document.createElement("span");
3669
+ errorSpan.className = "error-message";
3670
+ errorSpan.textContent = error;
3671
+ formField.appendChild(errorSpan);
3672
+ }
3673
+ } else {
3674
+ // Remove error message if exists
3675
+ if (errorSpan) {
3676
+ errorSpan.remove();
3677
+ }
3678
+ }
3679
+ }
3680
+
3681
+ // ==================== LIFECYCLE METHODS ====================
3682
+
3683
+ connectedCallback() {
3684
+ // Component is added to the DOM
3685
+ }
3686
+
3687
+ disconnectedCallback() {
3688
+ // Component is removed from the DOM
3689
+ // Clean up escape key handler
3690
+ if (this._escapeHandler) {
3691
+ document.removeEventListener("keydown", this._escapeHandler);
3692
+ }
3693
+ }
3694
+
3695
+ attributeChangedCallback(name, oldValue, newValue) {
3696
+ // Handle on-success attribute
3697
+ if (name === "on-success" && newValue) {
3698
+ // Use the setter to assign the callback from window scope
3699
+ this.onSuccess = window[newValue];
3700
+ }
3701
+
3702
+ // Handle on-error attribute
3703
+ if (name === "on-error" && newValue) {
3704
+ // Use the setter to assign the callback from window scope
3705
+ this.onError = window[newValue];
3706
+ }
3707
+
3708
+ // Handle on-submit attribute
3709
+ if (name === "on-submit" && newValue) {
3710
+ // Use the setter to assign the callback from window scope
3711
+ this.onSubmit = window[newValue];
3712
+ }
3713
+
3714
+ // Handle on-load attribute (expects JSON string or global variable name)
3715
+ if (name === "on-load" && newValue) {
3716
+ try {
3717
+ // Try to parse as JSON first
3718
+ const data = JSON.parse(newValue);
3719
+ this.onLoad = data;
3720
+ } catch (e) {
3721
+ // If not JSON, try to get from window scope (global variable)
3722
+ if (window[newValue]) {
3723
+ this.onLoad = window[newValue];
3724
+ }
3725
+ }
3726
+ }
3727
+ }
3728
+
3729
+ adoptedCallback() {
3730
+ // Component moved to new document
3731
+ }
3732
+ }
3733
+
3734
+ // Register the custom element only if it hasn't been registered yet
3735
+ if (!customElements.get("operator-onboarding")) {
3736
+ customElements.define("operator-onboarding", OperatorOnboarding);
3737
+ }
3738
+
3739
+ // Export for module usage (ES6)
3740
+ export { OperatorOnboarding };
3741
+
3742
+ // Make available globally for script tag usage
3743
+ if (typeof window !== "undefined") {
3744
+ window.OperatorOnboarding = OperatorOnboarding;
3745
+ }
3746
+
3747
+ // Export for CommonJS (Node.js)
3748
+ if (typeof module !== "undefined" && module.exports) {
3749
+ module.exports = { OperatorOnboarding };
3750
+ }