1const FINANCE_ELIGIBILITY_UK_DIAL = Object.freeze({ 2 iso: 'gb', 3 dial: '44', 4}); 5 6/** 7 * The phone field is a single mobile input with no flag or dial-code prefix, so the UK country and 8 * dial code the eligibility payload expects are held on hidden inputs and set here. 9 */ 10function applyFinanceEligibilityUkDial() { 11 const { iso, dial } = FINANCE_ELIGIBILITY_UK_DIAL; 12 const dialHidden = document.getElementById('cars-fe-phone-dial'); 13 const countryHidden = document.getElementById('cars-fe-phone-country'); 14 15 if (dialHidden) { 16 dialHidden.value = dial; 17 } 18 if (countryHidden) { 19 countryHidden.value = iso; 20 } 21} 22 23/** Sorted list for employment occupation comboboxes (search + pick). */ 24const FINANCE_ELIGIBILITY_OCCUPATIONS = [ 25 'Abattoir Worker','Abbot or Abbess','Abstractor','Accommodation Officer','Accompanist','Account Director','Account Executive','Account Manager','Accountant', 26 'Accounts Assistant','Accounts Clerk','Accounts Manager','Accounts Staff','Acoustic Engineer','Acrobat','Actor','Actuary','Acupuncturist', 27 'Administration Assistant','Administration Clerk','Administration Manager','Administration Officer','Administration Staff','Administrator',
28 'Advertising Agent','Advertising Assistant','Advertising Buyer','Advertising Clerk','Advertising Contractor','Advertising Controller','Advertising Director', 29 'Advertising Executive', 30 'Advertising Manager', 31 'Advertising Staff', 32 'Advocate', 33 'Aerial Erector', 34 'Aerobic Instructor', 35 'Aeronautical Engineer', 36 'After Dinner Speaker', 37 'Agister', 38 'Agricultural Consultant', 39 'Agricultural Contractor', 40 'Agricultural Engineer', 41 'Agricultural Merchant', 42 'Agricultural Worker', 43 'Agriculturalist', 44 'Agronomist', 45 'Air Force Crew', 46 'Air Traffic Controller', 47 'Aircraft Buyer', 48 'Aircraft Cabin Crew', 49 'Aircraft Designer', 50 'Aircraft Engineer', 51 'Aircraft Fitter', 52 'Aircraft Hand', 53 'Aircraft Maintenance Engineer', 54 'Aircraft Surface Finisher', 55 'Airline Broker', 56 'Airline Check-in Staff', 57 'Airport Controller', 58 'Airport Manager', 59 'Alarm Fitter', 60 'Almoner', 61 'Ambulance Controller', 62 'Ambulance Crew', 63 'Ambulance Driver', 64 'Amusement Arcade Worker', 65 'Anaesthetist', 66 'Analytical Chemist', 67 'Animal Breeder', 68 'Animator', 69 'Announcer', 70 'Anthropologist', 71 'Antique Dealer', 72 'Antique Renovator', 73 'Appeal/Tribunal Chairperson', 74 'Applications Engineer', 75 'Applications Programmer', 76 'Aquarist', 77 'Arbitrator', 78 'Arborist', 79 'Archaeologist', 80 'Archbishop', 81 'ArchDeacon', 82 'Architect', 83 'Architects Technician', 84 'Architectural Surveyor', 85 'Archivist', 86 'Area Manager', 87 'Armourer', 88 'Army - NCO/Commissioned Officer', 89 'Army - Other Ranks', 90 'Aromatherapist', 91 'Art Buyer', 92 'Art Critic', 93 'Art Dealer', 94 'Art Director', 95 'Art Historian', 96 'Art Restorer', 97 'Artexer', 98 'Articled Clerk', 99 'Artificial Inseminator', 100 'Artificial Intelligence Engineer', 101 'Artificial Limb Fitter', 102 'Artist - Commercial', 103 'Artist - Freelance', 104 'Artist - Portrait', 105 'Artist - Technical', 106 'Asbestos Remover', 107 'Asphalter', 108 'Assembly Worker', 109 'Assessor', 110 'Assistant Accounts Manager', 111 'Assistant Caretaker', 112 'Assistant Cook', 113 'Assistant Manager', 114 'Assistant Nurse', 115 'Assistant Teacher', 116 'Astrologer', 117 'Astronomer', 118 'Athlete', 119 'ATM Fitter', 120 'Au Pair', 121 'Auction Worker', 122 'Auctioneer', 123 'Audiologist', 124 'Audit Clerk', 125 'Audit Manager', 126 'Auditor', 127 'Author', 128 'Auto Electrician', 129 'Auxiliary Nurse', 130 'Bacon Curer', 131 'Bacteriologist', 132 'Baggage Handler', 133 'Bailiff', 134 'Baker', 135 'Bakery Assistant', 136 'Bakery Manager', 137 'Bakery Operator', 138 'Ballistics Expert', 139 'Balloonist', 140 'Bank Clerk', 141 'Bank Manager', 142 'Bank Messenger', 143 'Bank Note Checker', 144 'Bank Staff', 145 'Banking Correspondent', 146 'Baptist Minister', 147 'Bar Manager', 148 'Bar Staff', 149 'Bar Steward', 150 'Barber', 151 'Barge Operator', 152 'Barista', 153 'Barrister', 154 'Basket Worker', 155 'Beautician', 156 'Beauty Therapist', 157 'BeeKeeper', 158 'Betting Shop Clerk', 159 'Bill Poster', 160 'Bingo Caller', 161 'Bingo Hall Staff', 162 'Biochemist', 163 'Biologist', 164 'Biometrician', 165 'Biophysicist', 166 'Bishop', 167 'Blacksmith', 168 'Blind Assembler', 169 'Blind Fitter', 170 'Blinds Installer', 171 'Blogger/Vlogger', 172 'Boat Builder', 173 'Boatswain', 174 'Body Fitter', 175 'Bodyguard', 176 'Bodyshop Manager', 177 'Boiler Maker', 178 'Boiler Technician', 179 'Book Binder', 180 'Book Seller', 181 'Book-Keeper', 182 'Bookfinisher', 183 'Booking Agent', 184 'Booking Clerk', 185 'Booking Office Clerk', 186 'Boom Operator', 187 'Botanist', 188 'Bottler', 189 'Box Maker', 190 'Box Office Clerk', 191 'Boxer', 192 'Branch Manager', 193 'Brewer', 194 'Brewery Manager', 195 'Brewery Worker', 196 'Bricklayer', 197 'Bridge Operator', 198 'Bridgemaster', 199 'Broadcaster', 200 'Broadcasting Engineer', 201 'Brother or Sister (Church)', 202 'Builder', 203 'Builders Labourer', 204 'Builders Merchant', 205 'Building Advisor', 206 'Building Contractor', 207 'Building Control Officer', 208 'Building Engineer', 209 'Building Estimator', 210 'Building Foreperson', 211 'Building Inspector', 212 'Building Maintenance', 213 'Building Manager', 214 'Building Site Inspector', 215 'Building Society Clerk', 216 'Building Society Staff', 217 'Building Surveyor', 218 'Bursar', 219 'Bus Driver', 220 'Bus Mechanic', 221 'Bus Valeter', 222 'Business Analyst', 223 'Business Consultant', 224 'Butcher', 225 'Butchery Manager', 226 'Butler', 227 'Buyer', 228 'Cabinet Maker', 229 'Cable Contractor', 230 'Cable Jointer', 231 'Cable TV Installer', 232 'Cafe Owner', 233 'Cafe Staff', 234 'Calibration Manager', 235 'Call Centre Manager', 236 'Call Centre Staff', 237 'Calligrapher', 238 'Camera Operator/Technician', 239 'Camera Repairer', 240 'Canal Boat Broker', 241 'Canon or Canoness (Church)', 242 'Canvasser', 243 'Car Body Repairer', 244 'Car Builder', 245 'Car Delivery Driver', 246 'Car Park Attendant', 247 'Car Salesperson', 248 'Car Wash Attendant', 249 'Cardinal', 250 'Cardiographer', 251 'Cardiologist', 252 'Care Assistant', 253 'Care Manager', 254 'Careers Advisor', 255 'Careers Officer', 256 'Carer - Non Professional', 257 'Carer - Professional', 258 'Caretaker', 259 'Cargo Handler', 260 'Cargo Operator', 261 'Carpenter', 262 'Carpenters Assistant', 263 'Carpet Cleaner', 264 'Carpet Retailer', 265 'Carpetfitter', 266 'Cartographer', 267 'Cartoonist', 268 'Cashier', 269 'Cataloguer', 270 'Caterer', 271 'Catering Consultant', 272 'Catering Manager', 273 'Catering Staff', 274 'Caulker', 275 'Ceiling Contractor', 276 'Ceiling Fitter', 277 'Ceiling Fixer', 278 'Cellarperson', 279 'Certified Accountant', 280 'Chandler', 281 'Chaplain', 282 'Charge Hand', 283 'Chartered Accountant', 284 'Chartered Engineer', 285 'Chartered Surveyor', 286 'Chartered Valuer', 287 'Charterer', 288 'Chauffeur', 289 'Chef', 290 'Chemical Engineer', 291 'Chemist', 292 'Chicken Chaser', 293 'Chicken Sexer', 294 'Chief Cashier', 295 'Chief Chemist', 296 'Chief Executive', 297 'Chief Financial Officer', 298 'Chief Operating Officer', 299 'Chief Technical Officer', 300 'Child Care Officer', 301 'Child Minder', 302 'Childrens Entertainer', 303 'Chimney Sweep', 304 'China Restorer', 305 'Chiropodist', 306 'Chiropractor', 307 'Choir Leader', 308 'Choreographer', 309 'Chorister', 310 'Church Army Worker', 311 'Church Dean', 312 'Church Officer', 313 'Church Official', 314 'Church Warden', 315 'Cinema Assistant', 316 'Cinema Manager', 317 'Circus Proprietor', 318 'Circus Worker', 319 'Civil Engineer', 320 'Civil Servant', 321 'Claims Adjuster', 322 'Claims Assessor', 323 'Claims Manager', 324 'Clairvoyant', 325 'Clapper', 326 'Classical Musician',
327 'Clay Pigeon Instructor', 328 'Cleaner', 329 'Cleaning Contractor', 330 'Cleaning Supervisor', 331 'Clergy', 332 'Cleric', 333 'Clerical Assistant', 334 'Clerical Officer', 335 'Clerk', 336 'Clerk of Court', 337 'Clerk Of Works', 338 'Clinical Psychologist', 339 'Clock Maker', 340 'Clothing Design Cutter', 341 'Coach Builder', 342 'Coach Driver', 343 'Coach Sprayer', 344 'Coal Merchant', 345 'Coastguard', 346 'Cobbler', 347 'Coffee Merchant', 348 'Coin Dealer', 349 'College Dean', 350 'College Lecturer', 351 'College Principal', 352 'College Scout', 353 'Colourist (Maps/Photographs)', 354 'Comedian', 355 'Commercial Manager', 356 'Commission Agent', 357 'Commissionaire', 358 'Commissioner of Police', 359 'Commissioning Engineer', 360 'Commodity Broker', 361 'Commodity Dealer', 362 'Communications Officer', 363 'Communications Supervisor', 364 'Community Craft Instructor', 365 'Community Nurse', 366 'Community Worker', 367 'Companion', 368 'Companion Nurse', 369 'Company Chairperson', 370 'Company Director', 371 'Company Search Agent', 372 'Company Secretary', 373 'Compiler', 374 'Complementary Therapist', 375 'Compliance Officer', 376 'Composer', 377 'Compositor', 378 'Computer Analyst', 379 'Computer Consultant', 380 'Computer Editor', 381 'Computer Engineer', 382 'Computer Manager', 383 'Computer Network Controller', 384 'Computer Operator', 385 'Computer Programmer', 386 'Computer Technician', 387 'Conductor', 388 'Confectioner', 389 'Conference Manager', 390 'Conference Organiser', 391 'Conservationist', 392 'Conservator', 393 'Construction Engineer', 394 'Construction Worker', 395 'Consultant Engineer', 396 'Consumer Scientist', 397 'Contract Cleaner', 398 'Contract Furnisher', 399 'Contract Manager', 400 'Contracts Supervisor', 401 'Conveyancer', 402 'Cook', 403 'Cooper', 404 'Copier', 405 'Coppersmith', 406 'Copy Typist', 407 'Copywriter', 408 'Coroner', 409 'Corrosion Consultant', 410 'Costume Designer', 411 'Costume Jeweller', 412 'Costumier', 413 'Counter Staff', 414 'Countryside Ranger', 415 'County Councillor', 416 'Courier', 417 'Court Officer', 418 'Court Reporter', 419 'Coxswain', 420 'Craft Dealer', 421 'Craftsperson', 422 'Crane Driver', 423 'Crane Erector', 424 'Crane Operator', 425 'Crash Assessor', 426 'Creative Director', 427 'Creche Worker', 428 'Credit Broker', 429 'Credit Controller', 430 'Credit Manager', 431 'Credit Researcher', 432 'Crematorium Attendant', 433 'Cricketer', 434 'Crime Examiner', 435 'Criminologist', 436 'Crofter', 437 'Croupier', 438 'Crown Prosecutor', 439 'Curate', 440 'Curator', 441 'Currency Trader', 442 'Curtain Hanger', 443 'Curtain Maker', 444 'Customer Advisor', 445 'Customer Liaison Officer', 446 'Customer Service Controller', 447 'Customer Services Clerk', 448 'Customs & Excise Officer', 449 'Cutler', 450 'Cutter', 451 'Cycle Repairer', 452 'Cyclist', 453 'Dairy Engineer', 454 'Dairy Worker', 455 'Dance Teacher', 456 'Dancer', 457 'Dark Room Technician', 458 'Darts Player', 459 'Data Administrator', 460 'Data Co-Ordinator', 461 'Data Processing Manager', 462 'Data Processor', 463 'Data Scientist', 464 'Day Care Officer', 465 'Day Centre Officer', 466 'Dealer - General', 467 'Debt Collector', 468 'Debt Counsellor', 469 'Decorator', 470 'Delivery Courier', 471 'Delivery Driver', 472 'Delivery Rounds Person', 473 'Demolition Contractor', 474 'Demolition Worker', 475 'Demonstrator', 476 'Dendrochronologist', 477 'Dental Assistant', 478 'Dental Hygienist', 479 'Dental Mechanic', 480 'Dental Nurse', 481 'Dental Surgeon', 482 'Dental Technician', 483 'Dental Therapist', 484 'Dentist', 485 'Deputy Head Teacher', 486 'Deputy Manager', 487 'Deputy Principal', 488 'Dermatologist', 489 'Design Copier', 490 'Design Director', 491 'Design Engineer', 492 'Design Manager', 493 'Designer', 494 'Despatch Driver', 495 'Despatch Rider', 496 'Despatch Worker', 497 'Development Manager', 498 'Diamond Dealer', 499 'Diecaster', 500 'Dietician', 501 'Diplomat', 502 'Director of Environment', 503 'Director of Housing', 504 'Director of Planning', 505 'Disc Jockey', 506 'Distillery Worker', 507 'Distribution Manager', 508 'Distributor (Leaflets/Circ.)', 509 'District Nurse', 510 'District Valuer', 511 'Diver', 512 'Dock Pilot', 513 'Docker', 514 'Dockyard Worker', 515 'Doctor', 516 'Document Controller', 517 'Dog Beautician', 518 'Dog Breeder', 519 'Dog Groomer', 520 'Dog Handler', 521 'Dog Trainer', 522 'Dog Walker', 523 'Dog Warden', 524 'Doll Maker', 525 'Domestic Servant', 526 'Domestic Staff', 527 'Door Fitter', 528 'Door To Door Collector', 529 'Doorperson', 530 'Double Glazing Fitter', 531 'Double Glazing Salesperson', 532 'Draper', 533 'Draughtsperson', 534 'Drayperson', 535 'Dredger Master', 536 'Dredging Operator', 537 'Dresser Theatre/Films', 538 'Dressmaker', 539 'Driller (Civil Engineering)', 540 'Drilling Technician', 541 'Driver\'s Mate', 542 'Driving Examiner', 543 'Driving Instructor', 544 'Driving Instructor (HGV)', 545 'Driving Instructor - Advanced', 546 'Drone Operator', 547 'Drug Addiction Counsellor', 548 'Dry Cleaner', 549 'Dryliner', 550 'Dye Polisher', 551 'Dyer', 552 'Earth Moving Contractor', 553 'Ecologist', 554 'Economist', 555 'Editor', 556 'Editorial Consultant', 557 'Editorial Staff', 558 'Education Advisor', 559 'Education Officer', 560 'Electrical Contractor', 561 'Electrical Contracts Manager', 562 'Electrical Engineer', 563 'Electrical Fitter', 564 'Electrical Process Worker', 565 'Electrician', 566 'Electrologist', 567 'Electronic Engineer', 568 'Electronics Supervisor', 569 'Electronics Technician', 570 'Electroplater', 571 'Electrotyper', 572 'Embalmer', 573 'Embassy Staff', 574 'Embroiderer', 575 'Embryologist', 576 'Endocrinologist', 577 'Energy Analyst', 578 'Engine Operator', 579 'Engine Room Operator', 580 'Engineer', 581 'Engine Stoker', 582 'Engraver', 583 'Enquiry Agent', 584 'Entertainer', 585 'Entomologist', 586 'Environmental Chemist',
587 'Environmental Consultant', 588 'Environmental Health Officer', 589 'Equity Agent', 590 'Erector', 591 'Ergonomist', 592 'Escort', 593 'Estate Agent', 594 'Estate Manager', 595 'Estimator', 596 'Evangelist', 597 'Events Organiser', 598 'Examiner', 599 'Excursion Manager', 600 'Executive Officer', 601 'Exhaust Fitter', 602 'Exhibition Designer', 603 'Exhibition Organiser', 604 'Exotic Dancer', 605 'Expedition Leader', 606 'Explosives Worker', 607 'Export Consultant', 608 'Exporter', 609 'Extra', 610 'Extrusion Operator', 611 'Fabricator', 612 'Factory Canteen Manager', 613 'Factory Inspector', 614 'Factory Manager', 615 'Factory Worker', 616 'Fairground Worker', 617 'Falconer', 618 'Farm Manager', 619 'Farm Worker', 620 'Farrier', 621 'Fashion Designer', 622 'Fashion Photographer', 623 'Fast Food Delivery Driver', 624 'Fast Food Proprietor', 625 'Fence Erector', 626 'Fettler (Foundry)', 627 'Fibre Glass Moulder', 628 'Field Officer', 629 'Figure Painter', 630 'Filing Clerk', 631 'Film Director', 632 'Film Editor', 633 'Film Editor (Cutter)', 634 'Film Gaffer', 635 'Film Location Manager', 636 'Film Producer', 637 'Film Technician', 638 'Finance Director', 639 'Finance Manager', 640 'Finance Officer', 641 'Financial Advisor', 642 'Financial Analyst', 643 'Financial Consultant', 644 'Financier', 645 'Finisher', 646 'Fire Officer', 647 'Fire Prevention Officer', 648 'Fire Protection Consultant', 649 'Fire Service Inspector', 650 'Firefighter', 651 'Fireplace Fitter', 652 'Firewood Merchant', 653 'First Aid Instructor', 654 'First Aid Worker', 655 'Fish Buyer', 656 'Fish Filleter', 657 'Fish Fryer', 658 'Fish Merchant', 659 'Fish Worker', 660 'Fisher', 661 'Fisheries Inspector', 662 'Fishery Manager', 663 'Fishmonger', 664 'Fitness Instructor', 665 'Fitter', 666 'Flagger', 667 'Fleet Manager', 668 'Flight Attendant', 669 'Flight Deck Crew', 670 'Flight Dispatcher', 671 'Floor Layer', 672 'Floor Manager', 673 'Florist', 674 'Flour Miller', 675 'Flower Arranger', 676 'Flying Instructor', 677 'Foam Convertor', 678 'Food Processor', 679 'Footballer', 680 'Forensic Scientist', 681 'Foreperson', 682 'Forest Ranger', 683 'Forester', 684 'Fork Lift Truck Driver', 685 'Fortune Teller', 686 'Forwarding Agent', 687 'Foster Parent', 688 'Foundry Moulder', 689 'Foundry Worker', 690 'Fraud Investigator', 691 'Freelance Photographer', 692 'French Polisher', 693 'Fruiterer', 694 'Fuel Merchant', 695 'Fund Manager', 696 'Fund Raiser', 697 'Funeral Director', 698 'Funeral Furnisher', 699 'Funfair Employee', 700 'Furnace Operator', 701 'Furniture Dealer', 702 'Furniture Remover', 703 'Furniture Restorer', 704 'Furrier', 705 'Gallery Owner', 706 'Gambler', 707 'Gambling Commission Inspector', 708 'Gamekeeper', 709 'Gaming Club Manager', 710 'Gaming Club Proprietor', 711 'Garage Attendant', 712 'Garage Foreperson', 713 'Garage Manager', 714 'Garda', 715 'Garden Designer', 716 'Gardener', 717 'Gas Fitter', 718 'Gas Mechanic', 719 'Gas Technician', 720 'Gate Keeper', 721 'Genealogist', 722 'General Manager', 723 'General Practitioner', 724 'Geneticist', 725 'Geographer', 726 'Geologist', 727 'Geophysicist', 728 'Geriatrician', 729 'Gilder', 730 'Glass Worker', 731 'Glazier', 732 'Goldsmith', 733 'Golf Caddy', 734 'Golf Club Professional', 735 'Golf Coach', 736 'Golfer', 737 'Goods Despatcher', 738 'Goods Handler', 739 'Governor', 740 'Granite Technician', 741 'Graphic Designer', 742 'Graphologist', 743 'Grave Digger', 744 'Gravel Merchant', 745 'Green Keeper', 746 'Greengrocer', 747 'Grocer', 748 'Groom', 749 'Ground Worker', 750 'Grounds Person', 751 'Guard', 752 'Guest House Proprietor', 753 'Guide', 754 'Gun Smith', 755 'Gynaecologist', 756 'Haematologist', 757 'Hair Stylist', 758 'Hairdresser', 759 'Handyperson', 760 'Harbour Master', 761 'Hardware Dealer', 762 'Haulage Contractor', 763 'Hawker', 764 'Head Greenkeeper', 765 'Head Groom', 766 'Head of Arts', 767 'Head of Trade', 768 'Head of Traffic', 769 'Headteacher', 770 'Health Advisor', 771 'Health And Safety Consultant', 772 'Health And Safety Officer', 773 'Health Care Assistant', 774 'Health Planner', 775 'Health Therapist', 776 'Health Visitor', 777 'Hearing Aid Technician', 778 'Hearing Therapist', 779 'Heating & Ventilation Engineer', 780 'Heating Engineer', 781 'Heating/Ventilation Engineer', 782 'Hedger/Ditcher', 783 'Helmsperson', 784 'Herbalist', 785 'Herdsperson', 786 'HGV Driver', 787 'HGV Mechanic', 788 'Highway Inspector', 789 'Hire Car Driver', 790 'Hirer', 791 'Histologist', 792 'Historian', 793 'Hod Carrier', 794 'Home Economist', 795 'Home Help', 796 'Homemaker', 797 'Homecare Manager', 798 'Homemaker (Stay at Home)', 799 'Homeopath', 800 'Hop Merchant', 801 'Horse Breeder', 802 'Horse Dealer (Non Sport)', 803 'Horse Dealer (Sport)', 804 'Horse Riding Instructor', 805 'Horse Trader', 806 'Horse Trainer', 807 'Horticultural Consultant', 808 'Horticulturalist', 809 'Hosiery Mechanic', 810 'Hosiery Worker', 811 'Hospital Consultant', 812 'Hospital Doctor', 813 'Hospital Manager', 814 'Hospital Orderly', 815 'Hospital Photographer', 816 'Hospital Technician', 817 'Hospital Warden', 818 'Hospital Worker', 819 'Host', 820 'Hot Foil Printer', 821 'Hotel Consultant', 822 'Hotel Worker', 823 'Hotelier', 824 'House Parent', 825 'House Sitter', 826 'Housekeeper', 827 'Housing Assistant', 828 'Housing Officer', 829 'Housing Supervisor', 830 'Human Resources Manager', 831 'Human Resources Staff', 832 'Hunt Master', 833 'Huntsperson', 834 'Hydro Geologist', 835 'Hygienist', 836 'Hypnotherapist', 837 'Hypnotist', 838 'Ice Cream Vendor', 839 'Illustrator', 840 'Immigration Officer', 841 'Immunologist', 842 'Import Consultant', 843 'Import/Export Dealer', 844 'Importer', 845 'Independent Means', 846 'Induction Moulder', 847 'Industrial Chemist', 848 'Industrial Consultant', 849 'Industrial Designer', 850 'Injection Moulder', 851 'Inland Revenue Officer', 852 'Insolvency Practitioner',
853 'Inspector', 854 'Inspector - Insurance', 855 'Installation Worker', 856 'Instructor', 857 'Instrument Engineer', 858 'Instrument Maker', 859 'Instrument Supervisor', 860 'Instrument Technician', 861 'Insurance Assessor', 862 'Insurance Collector', 863 'Insurance Consultant', 864 'Insurance Inspector', 865 'Insurance Representative', 866 'Interior Decorator', 867 'Interior Designer', 868 'Interpreter', 869 'Interviewer', 870 'Inventor', 871 'Investigator', 872 'Investment Advisor', 873 'Investment Banker', 874 'Investment Manager', 875 'Invigilator', 876 'Ironer', 877 'Ironer Finisher', 878 'Ironer Presser', 879 'Ironmonger', 880 'IT Consultant', 881 'IT Manager', 882 'IT Trainer', 883 'IT/Systems Architect', 884 'Janitor', 885 'Jazz Composer', 886 'Jeweller', 887 'Jewellery Consultant', 888 'Jockey', 889 'Joiner', 890 'Joinery Consultant', 891 'Jointer', 892 'Journalist', 893 'Journalist - Freelance', 894 'Journalistic Agent', 895 'Judge', 896 'Juggler', 897 'Junk Shop Proprietor', 898 'Justice Of The Peace', 899 'Keep Fit Instructor', 900 'Kennel Hand', 901 'Kiln Setter', 902 'Kilnperson (Glass/Ceramics)', 903 'Kissagram Person', 904 'Kitchen Worker', 905 'Knitter', 906 'Labelling Operator', 907 'Laboratory Analyst', 908 'Laboratory Assistant', 909 'Laboratory Attendant', 910 'Laboratory Manager', 911 'Laboratory Operative', 912 'Laboratory Supervisor', 913 'Laboratory Technician', 914 'Labourer', 915 'Laminator', 916 'Lampshade Maker', 917 'Land Agent', 918 'Land Surveyor', 919 'Landowner', 920 'Landscape Architect', 921 'Landscape Gardener', 922 'Landworker', 923 'Lathe Operator', 924 'Laundry Worker', 925 'Lavatory Attendant', 926 'Law Clerk', 927 'Lawn Mower Repairer', 928 'Lawyer', 929 'Leaflet Distributor', 930 'Leather Worker', 931 'Lecturer', 932 'Ledger Clerk', 933 'Legal Advisor', 934 'Legal Assistant', 935 'Legal Executive', 936 'Legal Secretary', 937 'Leisure Centre Attendant', 938 'Leisure Centre Manager', 939 'Lengthsperson', 940 'Lens Grinder & Polisher', 941 'Letting Agent', 942 'Lexicographer', 943 'Liaison Officer', 944 'Librarian', 945 'Library Manager', 946 'Licensee', 947 'Licensing Consultant', 948 'Life Assurance Salesperson', 949 'Lifeguard', 950 'Lift Attendant', 951 'Lift Engineer', 952 'Lighterperson', 953 'Lighthouse Keeper', 954 'Lighting Designer', 955 'Lighting Director', 956 'Lighting Technician', 957 'Lime Kiln Attendant', 958 'Line Manager', 959 'Line Worker', 960 'Linesperson', 961 'Linguist', 962 'Linotype Operator', 963 'Literary Agent', 964 'Literary Editor', 965 'Lithographer', 966 'Litigation Manager', 967 'Loader', 968 'Loans Manager', 969 'Local Government Officer', 970 'Lock Keeper', 971 'Locksmith', 972 'Locum Doctor', 973 'Locum Pharmacist', 974 'Log Merchant', 975 'Lorry Driver', 976 'Loss Adjustor', 977 'Loss Assessor', 978 'Lumberjack', 979 'Machine Fitters Mate', 980 'Machine Minder', 981 'Machine Operator', 982 'Machine Setter', 983 'Machine Technician', 984 'Machine Tool Engineer', 985 'Machine Tool Fitter', 986 'Machinist', 987 'Magician', 988 'Magistrate', 989 'Magistrates Clerk', 990 'Maid', 991 'Mail Order Worker', 992 'Maintenance Engineer', 993 'Maintenance Fitter', 994 'Maintenance Manager', 995 'Maintenance Staff', 996 'Maitre D Hotel', 997 'Make Up Artist', 998 'Make Up Supervisor', 999 'Management Consultant', 1000 'Manager', 1001 'Manager - Sports', 1002 'Managing Clerk', 1003 'Managing Director', 1004 'Manicurist', 1005 'Manufacturing Agent', 1006 'Manufacturing Engineer', 1007 'Manufacturing Technician', 1008 'Map Maker', 1009 'Map Mounter', 1010 'Marble Finisher', 1011 'Marble Mason', 1012 'Marine Broker', 1013 'Marine Consultant', 1014 'Marine Electrician', 1015 'Marine Engineer', 1016 'Marine Geologist', 1017 'Marine Pilot', 1018 'Marine Surveyor', 1019 'Market Gardener', 1020 'Market Research Assistant', 1021 'Market Researcher', 1022 'Market Trader', 1023 'Marketing Agent', 1024 'Marketing Assistant', 1025 'Marketing Co-ordinator', 1026 'Marketing Consultant', 1027 'Marketing Director', 1028 'Marketing Executive', 1029 'Marketing Manager', 1030 'Marquee Erector', 1031 'Massage Therapist', 1032 'Master Mariner', 1033 'Master of Ceremonies', 1034 'Master Of Foxhounds', 1035 'Mate (Ship\'s)', 1036 'Materials Controller', 1037 'Materials Manager', 1038 'Mathematician', 1039 'Matron', 1040 'Mattress Maker', 1041 'Mature Student - Living At Home', 1042 'Mature Student - Living Away', 1043 'Meat Inspector', 1044 'Meat Wholesaler', 1045 'Mechanic', 1046 'Mechanical Designer', 1047 'Mechanical Engineer', 1048 'Mechanical Technician', 1049 'Medal Dealer', 1050 'Media Critic', 1051 'Media Planner', 1052 'Medical Advisor', 1053 'Medical Assistant', 1054 'Medical Consultant', 1055 'Medical Diagnostician', 1056 'Medical Officer', 1057 'Medical Physicist', 1058 'Medical Practitioner', 1059 'Medical Researcher', 1060 'Medical Secretary', 1061 'Medical Student - Living At Home', 1062 'Medical Student - Living Away', 1063 'Medical Supplier', 1064 'Medical Technician', 1065 'Member Of Parliament', 1066 'Member of Religious Order', 1067 'Merchandiser', 1068 'Merchant Banker', 1069 'Merchant Ship Captain', 1070 'Merchant Ship Crew', 1071 'Messenger', 1072 'Metal Dealer', 1073 'Metal Engineer', 1074 'Metal Polisher', 1075 'Metal Worker', 1076 'Metallurgist', 1077 'Meteorologist', 1078 'Meter Reader', 1079 'Microbiologist', 1080 'Microfilm Operator', 1081 'Microscopist', 1082 'Midwife', 1083 'Milk Delivery Person', 1084 'Mill Operator', 1085 'Mill Worker', 1086 'Miller', 1087 'Milliner', 1088 'Millwright', 1089 'Miner', 1090 'Mineralologist', 1091 'Minibus Driver', 1092 'Minicab Driver', 1093 'Mining Consultant', 1094 'Mining Engineer', 1095 'Minister Of Religion', 1096 'Minister Of The Crown', 1097 'Missionary', 1098 'Mobile Caterer', 1099 'Mobile Disc Jockey', 1100 'Mobile Disco Owner', 1101 'Mobile Hairdresser', 1102 'Mobile Motor Mechanic', 1103 'Mobile Service Engineer', 1104 'Model', 1105 'Model Maker', 1106 'Moderator', 1107 'Money Broker', 1108 'Money Dealer', 1109 'Moneylender', 1110 'Monk or Nun', 1111 'Monumental Sculptor', 1112 'Mooring Contractor', 1113 'Mortgage Broker', 1114 'Mortgage Consultant', 1115 'Mortician', 1116 'Motor Dealer', 1117 'Motor Engineer', 1118 'Motor Fitter', 1119 'Motor Mechanic', 1120 'Motor Racing Driver', 1121 'Motor Racing Organiser', 1122 'Motor Trader', 1123 'Moulding Process Technician', 1124 'Museum Assistant', 1125 'Museum Attendant', 1126 'Museum Consultant', 1127 'Museum Technician', 1128 'Music Producer', 1129 'Music Teacher', 1130 'Music Therapist', 1131 'Music Wholesaler', 1132 'Musical Arranger', 1133 'Musician', 1134 'Musician - Amateur', 1135 'Nanny', 1136 'Naturalist', 1137 'Naturopath', 1138 'Navigator', 1139 'Negotiator', 1140 'Neurologist', 1141 'Newsagent', 1142 'Newsreader', 1143 'Night Porter', 1144 'Night Watchperson', 1145 'Non Commissioned Officer', 1146 'Non Religious Celebrant', 1147 'Not Employed Due to Disability', 1148 'Not In Employment', 1149 'Notary Public', 1150 'Nuclear Scientist', 1151 'Nurse', 1152 'Nursemaid', 1153 'Nursery Nurse', 1154 'Nursery Worker (Childcare)', 1155 'Nursery Worker (Gardening)', 1156 'Nursing Assistant', 1157 'Nursing Auxiliary', 1158 'Nursing Manager', 1159 'Nursing Officer', 1160 'Nursing Sister', 1161 'Nutritionist', 1162 'Obstetrician', 1163 'Occupational Health Consultant', 1164 'Occupational Health Nurse', 1165 'Occupational Therapist', 1166 'Oceanographer', 1167 'Oculist', 1168 'Off Shore Surveyor', 1169 'Office Administrator', 1170 'Office Manager', 1171 'Oil Broker', 1172 'Oil Rig Crew', 1173 'On Benefits', 1174 'Online/eTrade Seller (e.g. Ebay)', 1175 'Opera Singer', 1176 'Operating Department Practitioner', 1177 'Operations Director', 1178 'Operations Engineer', 1179 'Operations Manager', 1180 'Operations Supervisor', 1181 'Ophthalmic Surgeon', 1182 'Ophthalmic Technician', 1183 'Optical Advisor', 1184 'Optical Assistant', 1185 'Optical Technician', 1186 'Optician', 1187 'Optometrist', 1188 'Orchestra Leader', 1189 'Orchestral Violinist', 1190 'Order Clerk', 1191 'Orderly', 1192 'Organist', 1193 'Ornamental Blacksmith', 1194 'Ornithologist', 1195 'Orthopaedic Technician', 1196 'Orthoptist', 1197 'Osteopath', 1198 'Outdoor Pursuits Instructor', 1199 'Outfitter', 1200 'Outreach Worker', 1201 'Oven Cleaner', 1202 'Overhead Line Instructor', 1203 'Overhead Lineperson', 1204 'Overlocker', 1205 'Overseas Mailer', 1206 'Overwriter', 1207 'Packaging Consultant', 1208 'Packer', 1209 'Paediatrician', 1210 'Paint Consultant', 1211 'Paint Sprayer', 1212 'Paint Technologist', 1213 'Painter', 1214 'Painter And Decorator', 1215 'Palaeobotanist', 1216 'Palaeontologist', 1217 'Pallet Maker', 1218 'Panel Beater', 1219 'Paper Mill Worker', 1220 'Parachute Packer', 1221 'Paramedic', 1222 'Park Attendant', 1223 'Park Keeper', 1224 'Park Ranger', 1225 'Partition Erector', 1226 'Parts Manager', 1227 'Parts Person', 1228 'Parts Supervisor', 1229 'Party Planner', 1230 'Pasteuriser', 1231 'Pastry Chef', 1232 'Patent Agent', 1233 'Patent Attorney', 1234 'Pathologist', 1235 'Patrol Person', 1236 'Pattern Cutter', 1237 'Pattern Maker', 1238 'Pattern Weaver', 1239 'Paviour', 1240 'Pawnbroker', 1241 'Payment Officer', 1242 'Payroll Assistant', 1243 'Payroll Clerk', 1244 'Payroll Manager', 1245 'Payroll Supervisor', 1246 'Pearl Stringer', 1247 'Pedicurist', 1248 'Pensions Consultant', 1249 'Pensions Manager', 1250 'Personal Assistant', 1251 'Personnel Administrator', 1252 'Personnel Manager', 1253 'Personnel Officer', 1254 'Pest Control', 1255 'Pest Controller', 1256 'Pet Minder', 1257 'Petrol Station Attendant', 1258 'Petroleum Engineer', 1259 'Petty Officer', 1260 'Pharmaceutical Assistant', 1261 'Pharmacist', 1262 'Pharmacy Manager', 1263 'Pharmacy Technician', 1264 'Philatelist', 1265 'Phlebotomist', 1266 'Photo Engraver', 1267 'Photo Laboratory Processor', 1268 'Photo Technician', 1269 'Photocopy Machine Technician', 1270 'Photographer', 1271 'Photographer - Location', 1272 'Photographer - Press', 1273 'Photographer - Press (Local)', 1274 'Photographer - Shop Owner', 1275 'Photographer - Street', 1276 'Photographer - Studio', 1277 'Photographic Agent', 1278 'Physician', 1279 'Physicist', 1280 'Physiologist', 1281 'Physiotherapist', 1282 'Piano Teacher', 1283 'Piano Tuner', 1284 'Picker', 1285 'Picture Editor', 1286 'Picture Framer', 1287 'Picture Renovator', 1288 'Picture Researcher', 1289 'Pig Manager', 1290 'Pilot', 1291 'Pilot Helicopter', 1292 'Pilot Test', 1293 'Pipe Fitter', 1294 'Pipe Inspector', 1295 'Pipe Insulator', 1296 'Pipe Layer', 1297 'Planning Engineer', 1298 'Planning Manager', 1299 'Planning Officer', 1300 'Planning Technician', 1301 'Plant Attendant', 1302 'Plant Breeder', 1303 'Plant Driver', 1304 'Plant Engineer', 1305 'Plant Fitter', 1306 'Plant Manager', 1307 'Plant Operator', 1308 'Plasterer', 1309 'Plastics Consultant', 1310 'Plastics Engineer', 1311 'Plate Layer', 1312 'Plater', 1313 'Playgroup Assistant', 1314 'Playgroup Leader', 1315 'Playwright', 1316 'Plumber', 1317 'Plumbing & Heating Engineer', 1318 'Podiatrist', 1319 'Points Person', 1320 'Police - Civilian',
1321 'Police Community Support Officer', 1322 'Police Inspector', 1323 'Police Officer', 1324 'Polisher', 1325 'Pool Attendant', 1326 'Port Officer', 1327 'Porter', 1328 'Portfolio Manager', 1329 'Post Card Seller', 1330 'Post Graduate Student Living at Home', 1331 'Post Graduate Student Living Away from Home', 1332 'Post Office Counter Clerk', 1333 'Post Office Sub-Manager', 1334 'Post Sorter', 1335 'Postmaster or Mistress', 1336 'Postperson', 1337 'Potato Merchant', 1338 'Potter', 1339 'Poultry Worker', 1340 'Practice Manager', 1341 'Preacher', 1342 'Precious Metal Merchant', 1343 'Precision Engineer', 1344 'Premises Security Installers', 1345 'Press Correspondent', 1346 'Press Officer', 1347 'Press Operator', 1348 'Press Photographer - Freelance', 1349 'Press Photographer - National', 1350 'Press Producer', 1351 'Press Setter', 1352 'Presser', 1353 'Priest or Reverend', 1354 'Principal', 1355 'Print Finisher', 1356 'Printer', 1357 'Printing Press Operator', 1358 'Prison Chaplain', 1359 'Prison Officer', 1360 'Private Detective', 1361 'Private Investigator', 1362 'Privy Councillor', 1363 'Probation Officer', 1364 'Probation Worker', 1365 'Process Engineer', 1366 'Process Operator', 1367 'Process Worker', 1368 'Procurator Fiscal', 1369 'Produce Supervisor', 1370 'Producer', 1371 'Product Designer', 1372 'Product Installer', 1373 'Product Manager', 1374 'Production Engineer', 1375 'Production Hand', 1376 'Production Manager', 1377 'Production Planner', 1378 'Professional Gamer', 1379 'Professor', 1380 'Progress Chaser', 1381 'Progress Clerk', 1382 'Project Co-ordinator', 1383 'Project Engineer', 1384 'Project Leader', 1385 'Project Manager', 1386 'Project Worker', 1387 'Projectionist', 1388 'Promoter', 1389 'Promoter - Entertainments', 1390 'Promoter - Racing', 1391 'Promoter - Ring Sports', 1392 'Promoter - Sports', 1393 'Proof Reader', 1394 'Property Buyer', 1395 'Property Dealer', 1396 'Property Developer', 1397 'Property Manager', 1398 'Property Owner (Letting)', 1399 'Property Valuer', 1400 'Proprietor', 1401 'Prosthetist', 1402 'Psychiatrist', 1403 'Psychoanalyst', 1404 'Psychodynamic Counsellor', 1405 'Psychologist', 1406 'Psychotherapist', 1407 'Public House Manager', 1408 'Public Relations Officer', 1409 'Publican', 1410 'Publicity Manager', 1411 'Publisher', 1412 'Publishing Manager', 1413 'Purchase Clerk', 1414 'Purchase Ledger Clerk', 1415 'Purchasing Assistant', 1416 'Purchasing Manager', 1417 'Purser', 1418 'Quality Controller', 1419 'Quality Engineer', 1420 'Quality Inspector', 1421 'Quality Manager', 1422 'Quality Technician', 1423 'Quantity Surveyor', 1424 'Quarry Worker', 1425 'Queens Counsel', 1426 'Rabbi', 1427 'Racehorse Groom', 1428 'Racing Motorcyclist', 1429 'Racing Organiser', 1430 'Radar Mechanic', 1431 'Radio Controller', 1432 'Radio Director', 1433 'Radio Engineer', 1434 'Radio Helpline Coordinator', 1435 'Radio Mechanic', 1436 'Radio Operator', 1437 'Radio Presenter', 1438 'Radio Producer', 1439 'Radiographer', 1440 'Radiologist', 1441 'Radiotherapist', 1442 'Rail Person', 1443 'Rally Driver', 1444 'Ramp Agent', 1445 'Re-Settlement Officer', 1446 'Reactor Attendant', 1447 'Reader', 1448 'Reader Compositor', 1449 'Receptionist', 1450 'Records Supervisor', 1451 'Recovery Vehicle Co-ordinator', 1452 'Recruitment Consultant', 1453 'Rector', 1454 'Referee', 1455 'Refit Merchandiser', 1456 'Reflexologist', 1457 'Refractory Engineer', 1458 'Refrigeration Engineer', 1459 'Refuse Collector', 1460 'Registered Disabled', 1461 'Registrar', 1462 'Regulator', 1463 'Religious Dignitary', 1464 'Religious Leader', 1465 'Relocation Agent', 1466 'Remedial Therapist', 1467 'Rent Collector', 1468 'Rent Officer', 1469 'Repairer', 1470 'Reporter', 1471 'Reporter - Freelance', 1472 'Reprographic Assistant', 1473 'Rescue Worker', 1474 'Research Analyst', 1475 'Research Assistant', 1476 'Research Consultant', 1477 'Research Director', 1478 'Research Scientist', 1479 'Research Technician', 1480 'Researcher', 1481 'Resin Caster', 1482 'Restaurant Manager', 1483 'Restaurateur', 1484 'Restorer', 1485 'Retired', 1486 'Revenue Clerk', 1487 'Revenue Officer', 1488 'Rheumatologist', 1489 'Rig Worker', 1490 'Rigger', 1491 'Riverperson', 1492 'Riveter', 1493 'Road Safety Officer', 1494 'Road Sweeper', 1495 'Road Worker', 1496 'Roof Tiler', 1497 'Roofer', 1498 'Rose Grower', 1499 'Royal Air Force - NCO/Commissioned Officer', 1500 'Royal Air Force - Other Ranks', 1501 'Royal Marine', 1502 'Royal Navy - NCO/Commissioned Officer', 1503 'Royal Navy - Other Ranks', 1504 'Rubber Worker', 1505 'Rug Maker', 1506 'Rugby Player', 1507 'Saddler', 1508 'Safety Officer', 1509 'Sail Maker', 1510 'Sales Administrator', 1511 'Sales Assistant', 1512 'Sales Director', 1513 'Sales Engineer', 1514 'Sales Executive', 1515 'Sales Manager', 1516 'Sales Representative',
1517 'Sales Support', 1518 'Salesperson', 1519 'Salvage Dealer', 1520 'Salvage Hand', 1521 'Salvation Army Officer', 1522 'Sample Hand', 1523 'Sand Blaster', 1524 'Sand Merchant', 1525 'Saw Miller', 1526 'Sawyer', 1527 'Scaffolder', 1528 'Scenehand', 1529 'School Catering Assistant', 1530 'School Counsellor', 1531 'School Crossing Warden', 1532 'School Inspector', 1533 'School Student', 1534 'Scientific Officer', 1535 'Scientist', 1536 'Scientist - Atomic Energy', 1537 'Scrap Dealer', 1538 'Screen Printer', 1539 'Screen Writer', 1540 'Script Writer', 1541 'Sculptor', 1542 'Sea Person', 1543 'Search Engine Optimization Analyst/Consultant/Executive', 1544 'Search Engine Optimization Manager', 1545 'Second Hand Dealer', 1546 'Secretary', 1547 'Secretary And PA', 1548 'Security Consultant', 1549 'Security Controller', 1550 'Security Guard', 1551 'Security Officer', 1552 'Seedsperson', 1553 'Servant', 1554 'Service Engineer', 1555 'Service Engineer (Non-Mobile)', 1556 'Service Manager', 1557 'Setter', 1558 'Sewage Worker', 1559 'Share Dealer', 1560 'Sheet Metal Worker', 1561 'Shelf Filler', 1562 'Shelter Warden', 1563 'Shepherd or Shepherdess', 1564 'Sheriff', 1565 'Sheriff Clerk', 1566 'Sheriff Principal', 1567 'Sheriffs Officer', 1568 'Shift Controller', 1569 'Ship Broker', 1570 'Ship Builder', 1571 'Shipping Clerk', 1572 'Shipping Officer', 1573 'Shipwright', 1574 'Shipyard Worker', 1575 'Shoe Maker', 1576 'Shoe Repairer', 1577 'Shooting Instructor', 1578 'Shop Assistant', 1579 'Shop Fitter', 1580 'Shop Keeper', 1581 'Shop Manager', 1582 'Shorthand Writer', 1583 'Shot Blaster', 1584 'Show Jumper', 1585 'Showperson', 1586 'Shunter', 1587 'Sign Maker', 1588 'Signalperson', 1589 'Signwriter', 1590 'Silversmith', 1591 'Singer', 1592 'Site Agent', 1593 'Site Engineer', 1594 'Skipper', 1595 'Slater', 1596 'Slaughterperson', 1597 'Smallholder', 1598 'Smelter', 1599 'Snooker Player', 1600 'Social Media Influencer', 1601 'Social Worker', 1602 'Sociologist', 1603 'Software Consultant', 1604 'Software Engineer', 1605 'Solar Panel Installer And Repairer', 1606 'Soldier', 1607 'Solicitor', 1608 'Song Writer', 1609 'Sorter', 1610 'Sound Editor', 1611 'Sound Engineer', 1612 'Sound Mixer', 1613 'Sound Technician', 1614 'Special Constable', 1615 'Special Needs Assistant', 1616 'Speech Therapist', 1617 'Sports Administrator', 1618 'Sports Agent', 1619 'Sports Centre Attendant', 1620 'Sports Coach', 1621 'Sports Commentator', 1622 'Sports Equipment Repairer', 1623 'Sports Scout', 1624 'Spring Maker', 1625 'Stable Hand', 1626 'Staff Nurse', 1627 'Stage Director', 1628 'Stage Manager', 1629 'Stage Mover', 1630 'Stamp Dealer', 1631 'State Enrolled Nurse', 1632 'State Registered Nurse', 1633 'Station Manager', 1634 'Stationer', 1635 'Statistician', 1636 'Steel Erector', 1637 'Steel Worker', 1638 'Steeplejack', 1639 'Stenographer', 1640 'Stevedore', 1641 'Stock Controller', 1642 'Stock Exchange Dealer', 1643 'Stock Manager', 1644 'Stockbroker', 1645 'Stockperson', 1646 'Stocktaker', 1647 'Stone Cutter', 1648 'Stone Sawyer', 1649 'Stonemason', 1650 'Store Detective', 1651 'Store Keeper', 1652 'Storeperson', 1653 'Stores Controller', 1654 'Street Entertainer', 1655 'Street Trader', 1656 'Stud Hand', 1657 'Student', 1658 'Student - Living at Home', 1659 'Student - Living Away', 1660 'Student Counsellor', 1661 'Student Nurse - Living At Home', 1662 'Student Nurse - Living Away', 1663 'Student Teacher - Living At Home', 1664 'Student Teacher - Living Away', 1665 'Studio Manager', 1666 'Supervisor', 1667 'Supply Teacher', 1668 'Support Worker', 1669 'Surgeon', 1670 'Surgeon - N.H.S.', 1671 'Surgeon - Non N.H.S.', 1672 'Surveyor', 1673 'Surveyor - Chartered', 1674 'Sustainability Specialist', 1675 'Systems Analyst', 1676 'Systems Engineer', 1677 'Systems Manager', 1678 'T-Shirt Printer', 1679 'Tachograph Analyst', 1680 'Tacker', 1681 'Tailor', 1682 'Tank Farm Operative', 1683 'Tanker Driver', 1684 'Tanner', 1685 'Tape Librarian', 1686 'Tape Operator', 1687 'Tarmacer', 1688 'Tarot Reader/Palmistry Expert', 1689 'Taster', 1690 'Tattooist', 1691 'Tax Advisor', 1692 'Tax Analyst', 1693 'Tax Assistant', 1694 'Tax Consultant', 1695 'Tax Inspector', 1696 'Tax Manager', 1697 'Tax Officer', 1698 'Taxi Controller', 1699 'Taxi Driver', 1700 'Taxidermist', 1701 'Tea Blender', 1702 'Tea Taster', 1703 'Teacher', 1704 'Teachers Assistant', 1705 'Technical Advisor', 1706 'Technical Analyst', 1707 'Technical Assistant', 1708 'Technical Author', 1709 'Technical Clerk', 1710 'Technical Co-ordinator', 1711 'Technical Director', 1712 'Technical Editor', 1713 'Technical Engineer', 1714 'Technical Illustrator', 1715 'Technical Instructor', 1716 'Technical Liaison Engineer', 1717 'Technical Manager', 1718 'Technician', 1719 'Technician - Performing Arts', 1720 'Technologist', 1721 'Telecommunication Consultant', 1722 'Telecommunications Engineer', 1723 'Telecommunications Manager', 1724 'Telemarketer', 1725 'Telephone Engineer', 1726 'Telephonist', 1727 'Telesales Person', 1728 'Television Director', 1729 'Television Engineer', 1730 'Television Presenter', 1731 'Television Producer', 1732 'Television Set Builder', 1733 'Temperature Time Recorder', 1734 'Tennis Coach', 1735 'Tennis Player', 1736 'Terrier', 1737 'Territorial Army Officer', 1738 'Test Driver', 1739 'Test Engineer', 1740 'Textile Consultant', 1741 'Textile Engineer', 1742 'Textile Technician', 1743 'Textile Worker', 1744 'Thatcher', 1745 'Theatre Director', 1746 'Theatre Manager', 1747 'Theatrical Agent', 1748 'Theme Park Worker', 1749 'Therapist', 1750 'Thermal Engineer', 1751 'Thermal Insulator', 1752 'Ticket Agent', 1753 'Ticket Collector', 1754 'Ticket Inspector', 1755 'Tiler', 1756 'Timber Inspector', 1757 'Timber Worker', 1758 'Time and Motion Analyst', 1759 'Tobacconist', 1760 'Toll Collector', 1761 'Tool Maker', 1762 'Tool Setter', 1763 'Tour Agent', 1764 'Tour Guide', 1765 'Town Clerk', 1766 'Town Planner', 1767 'Toy Maker', 1768 'Toy Trader', 1769 'Track Worker', 1770 'Tractor Driver', 1771 'Tractor Mechanic', 1772 'Trade Mark Agent', 1773 'Trade Union Official', 1774 'Trading Standards Officer', 1775 'Traffic Clerk', 1776 'Traffic Despatcher', 1777 'Traffic Engineer', 1778 'Traffic Officer', 1779 'Traffic Planner', 1780 'Traffic Supervisor', 1781 'Traffic Warden', 1782 'Train Despatcher', 1783 'Train Driver', 1784 'Train Motor Operative', 1785 'Trainee Manager', 1786 'Trainer', 1787 'Training Advisor', 1788 'Training Assistant', 1789 'Training Co-ordinator', 1790 'Training Consultant', 1791 'Training Instructor', 1792 'Training Manager', 1793 'Training Officer', 1794 'Transcriber', 1795 'Translator', 1796 'Transmission Controller', 1797 'Transport Clerk', 1798 'Transport Consultant', 1799 'Transport Controller', 1800 'Transport Engineer', 1801 'Transport Manager', 1802 'Transport Officer', 1803 'Transport Planner', 1804 'Transport Police Officer', 1805 'Travel Agent', 1806 'Travel Clerk', 1807 'Travel Consultant', 1808 'Travel Courier', 1809 'Travel Guide', 1810 'Travel Guide Writer', 1811 'Travel Representative', 1812 'Travelling Salesperson', 1813 'Trawler Hand', 1814 'Treasurer', 1815 'Tree Feller', 1816 'Tree Surgeon', 1817 'Trichologist', 1818 'Trinity House Pilot', 1819 'Trout Farmer', 1820 'Truck Driver', 1821 'Tug Skipper', 1822 'Tunneller', 1823 'Turf Accountant', 1824 'Turkey Farmer', 1825 'Turner', 1826 'Tutor', 1827 'TV And Video Installer', 1828 'TV And Video Repairer', 1829 'TV Announcer', 1830 'TV Broadcasting Technician', 1831 'TV Editor', 1832 'Typesetter', 1833 'Typewriter Engineer', 1834 'Typist', 1835 'Typographer', 1836 'Tyre Builder', 1837 'Tyre Fitter', 1838 'Tyre Inspector', 1839 'Tyre Technician', 1840 'Tyre/Exhaust Fitter', 1841 'Umpire', 1842 'Undergraduate Student - Living At Home', 1843 'Undergraduate Student - Living Away from Home', 1844 'Undertaker', 1845 'Underwriter', 1846 'Unemployed', 1847 'University Dean', 1848 'University Reader', 1849 'Upholsterer', 1850 'Usher', 1851 'Valet', 1852 'Valuer', 1853 'Valve Technician', 1854 'Van Driver', 1855 'Van Salesperson', 1856 'VDU Operator', 1857 'Vehicle Assessor', 1858 'Vehicle Bodybuilder', 1859 'Vehicle Delivery Agent', 1860 'Vehicle Engineer', 1861 'Vehicle Technician', 1862 'Vehicle Valet', 1863 'Vending Machine Filler', 1864 'Vending Machine Technician', 1865 'Ventriloquist', 1866 'Verger', 1867 'Veterinary Assistant', 1868 'Veterinary Nurse', 1869 'Veterinary Surgeon', 1870 'Vicar', 1871 'Violin Maker', 1872 'Vision Control Manager', 1873 'Vision Mixer', 1874 'Voluntary Worker', 1875 'Vulcanologist', 1876 'Wages Clerk', 1877 'Waiting Staff', 1878 'Warden', 1879 'Wardrobe Manager', 1880 'Warehouse Manager', 1881 'Warehouse Operative', 1882 'Waste Dealer', 1883 'Waste Disposal Worker', 1884 'Waste Paper Merchant', 1885 'Watchmaker', 1886 'Water Diviner', 1887 'Weaver', 1888 'Web Designer', 1889 'Web Developer', 1890 'Web Programmer', 1891 'Weighbridge Clerk', 1892 'Weighbridge Operator', 1893 'Welder', 1894 'Welfare Assistant', 1895 'Welfare Officer', 1896 'Welfare Rights Officer', 1897 'Wheel Clamper', 1898 'Wheelwright', 1899 'Whisky Blender',
1900 'Wholesale Newspaper Delivery Driver', 1901 'Will Writer', 1902 'Window Cleaner', 1903 'Window Dresser', 1904 'Windscreen Fitter', 1905 'Wine Merchant', 1906 'Wood Carver', 1907 'Wood Cutter', 1908 'Wood Machinist', 1909 'Wood Worker', 1910 'Word Processing Operator', 1911 'Work Study Analyst', 1912 'Works Manager', 1913 'Wrestler', 1914 'Writer', 1915 'Yacht Master', 1916 'Yard Manager', 1917 'Yard Person', 1918 'Yoga Teacher', 1919 'Youth Hostel Warden', 1920 'Youth Worker', 1921 'Zoo Keeper', 1922 'Zoo Manager', 1923 'Zoologist', 1924 'Zoology Consultant' 1925].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' })); 1926 1927function financeEligibilityFilterOccupations(query) { 1928 const q = String(query || '') 1929 .trim() 1930 .toLowerCase(); 1931 if (!q) { 1932 return FINANCE_ELIGIBILITY_OCCUPATIONS.slice(); 1933 } 1934 return FINANCE_ELIGIBILITY_OCCUPATIONS.filter((o) => o.toLowerCase().includes(q)); 1935} 1936 1937/** Scrollable region inside the active finance eligibility journey step. */ 1938function financeEligibilityFindScrollArea(fromEl) { 1939 const offcanvas = document.getElementById('financeEligibilityOffcanvas'); 1940 const step = 1941 fromEl?.closest?.('.cars-fe-journey-step') || 1942 offcanvas?.querySelector('.cars-fe-journey-step:not([hidden])'); 1943 if (!step || step.hidden) { 1944 return null; 1945 } 1946 return ( 1947 step.querySelector('.cars-fe-scroll') || 1948 step.querySelector('.finance-eligibility-intro__scroll') || 1949 step.querySelector('.finance-eligibility-agreement__scroll') 1950 ); 1951} 1952 1953function financeEligibilitySetOccupationDropdownOpen(open) { 1954 const stack = document.querySelector('.cars-fe-form-stack--employment'); 1955 const step = 1956 stack?.closest('.cars-fe-journey-step') || document.getElementById('cars-fe-step-employment'); 1957 if (!step) { 1958 return; 1959 } 1960 step.classList.toggle('cars-fe-occupation-dropdown-open', Boolean(open)); 1961 if (!open && stack) { 1962 delete stack.dataset.feScrollPadExtra; 1963 stack.style.removeProperty('padding-bottom'); 1964 } 1965} 1966 1967/** Scroll a field (and optional dropdown below it) so it stays above the sticky step footer on small screens. */ 1968function financeEligibilityScrollFieldIntoView(targetEl, options = {}) { 1969 if (!targetEl) { 1970 return; 1971 } 1972 const scrollEl = options.scrollArea || financeEligibilityFindScrollArea(targetEl); 1973 const anchor = 1974 targetEl.closest?.('.cars-fe-duration-listbox') || 1975 targetEl.closest?.('.cars-fe-occupation-combobox') || 1976 targetEl.closest?.('.cars-fe-field-group') || 1977 targetEl.closest?.('.cars-fe-field-group__shell') || 1978 targetEl; 1979 const behavior = options.behavior || 'smooth'; 1980 const gap = options.gapPx ?? 12; 1981 if (!scrollEl) { 1982 if (typeof anchor.scrollIntoView === 'function') { 1983 anchor.scrollIntoView({ behavior, block: 'nearest' }); 1984 } 1985 return; 1986 } 1987 1988 const listPanel = 1989 options.listPanelEl || 1990 anchor.querySelector?.('.cars-fe-duration-listbox__panel:not([hidden])') || 1991 anchor.querySelector?.('.cars-fe-occupation-combobox__panel:not([hidden])'); 1992 let reserveBelow = options.reserveBelowPx ?? 0; 1993 if (listPanel && listPanel.hidden === false) { 1994 reserveBelow = Math.max(reserveBelow, listPanel.getBoundingClientRect().height + gap); 1995 } else if (anchor.classList?.contains('cars-fe-occupation-combobox')) { 1996 reserveBelow = Math.max(reserveBelow, 220 + gap); 1997 } else if (anchor.classList?.contains('cars-fe-duration-listbox')) { 1998 reserveBelow = Math.max(reserveBelow, 220 + gap); 1999 } else if (targetEl.tagName === 'SELECT') { 2000 reserveBelow = Math.max(reserveBelow, 180 + gap); 2001 } 2002 2003 const areaRect = scrollEl.getBoundingClientRect(); 2004 const anchorRect = anchor.getBoundingClientRect(); 2005 const dropdownBottom = anchorRect.bottom + reserveBelow; 2006 let nextScrollTop = scrollEl.scrollTop; 2007 2008 if (dropdownBottom > areaRect.bottom - gap) { 2009 nextScrollTop += dropdownBottom - (areaRect.bottom - gap); 2010 } 2011 2012 const topPad = options.topPaddingPx ?? 16; 2013 const topTarget = anchorRect.top - areaRect.top + scrollEl.scrollTop - topPad; 2014 if (topTarget < nextScrollTop) { 2015 nextScrollTop = topTarget; 2016 } 2017 2018 const maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight); 2019 if (nextScrollTop > maxScroll) { 2020 const stack = 2021 scrollEl.querySelector('.cars-fe-form-stack') || 2022 scrollEl.querySelector('.cars-fe-form-stack--employment') || 2023 scrollEl; 2024 const extra = nextScrollTop - maxScroll + gap; 2025 const prev = parseFloat(stack.dataset.feScrollPadExtra || '0') || 0; 2026 const newPad = prev + extra; 2027 stack.dataset.feScrollPadExtra = String(newPad); 2028 stack.style.paddingBottom = `${newPad}px`; 2029 nextScrollTop = scrollEl.scrollHeight - scrollEl.clientHeight; 2030 } 2031 2032 scrollEl.scrollTo({ 2033 top: Math.max(0, nextScrollTop), 2034 behavior, 2035 }); 2036} 2037
2038function financeEligibilityScrollOccupationDropdownIntoView(queryInput, listPanel) { 2039 if (!queryInput) { 2040 return; 2041 } 2042 financeEligibilityScrollFieldIntoView(queryInput, { 2043 listPanelEl: listPanel, 2044 topPaddingPx: 8, 2045 gapPx: 16, 2046 behavior: 'smooth', 2047 }); 2048} 2049 2050function financeEligibilityScheduleFieldScrollIntoView(targetEl, options = {}) { 2051 if (!targetEl) { 2052 return; 2053 } 2054 window.requestAnimationFrame(() => { 2055 financeEligibilityScrollFieldIntoView(targetEl, options); 2056 window.requestAnimationFrame(() => { 2057 financeEligibilityScrollFieldIntoView(targetEl, options); 2058 }); 2059 }); 2060} 2061 2062function financeEligibilityCloseOccupationPanels(rootEl) { 2063 const root = rootEl || document.getElementById('cars-fe-step-employment'); 2064 if (!root) { 2065 return; 2066 } 2067 root.querySelectorAll('.cars-fe-occupation-combobox__panel').forEach((panel) => { 2068 if (panel.hidden) { 2069 return; 2070 } 2071 panel.hidden = true; 2072 const wrap = panel.closest('.cars-fe-occupation-combobox'); 2073 const qInp = wrap?.querySelector('.cars-fe-occupation-combobox__input'); 2074 if (qInp) { 2075 qInp.setAttribute('aria-expanded', 'false'); 2076 qInp.removeAttribute('aria-activedescendant'); 2077 } 2078 wrap?.querySelectorAll('.cars-fe-occupation-combobox__option.is-active').forEach((li) => li.classList.remove('is-active')); 2079 }); 2080} 2081 2082/** 2083 * @param {{ hiddenId: string; queryId: string; listboxId: string }} config 2084 * @param {() => void} [onChange] 2085 */ 2086function financeEligibilityAttachOccupationCombobox(config, onChange) { 2087 const hidden = document.getElementById(config.hiddenId); 2088 const query = document.getElementById(config.queryId); 2089 const listbox = document.getElementById(config.listboxId); 2090 const panel = query?.closest('.cars-fe-occupation-combobox')?.querySelector('.cars-fe-occupation-combobox__panel'); 2091 if (!hidden || !query || !listbox || !panel || query.dataset.feOccInit === '1') { 2092 return; 2093 } 2094 query.dataset.feOccInit = '1'; 2095 2096 let activeIdx = -1; 2097 2098 const notify = () => { 2099 if (typeof onChange === 'function') { 2100 onChange(); 2101 } 2102 }; 2103 2104 const setExpanded = (open) => { 2105 query.setAttribute('aria-expanded', open ? 'true' : 'false'); 2106 panel.hidden = !open; 2107 financeEligibilitySetOccupationDropdownOpen(open); 2108 if (!open) { 2109 activeIdx = -1; 2110 query.removeAttribute('aria-activedescendant'); 2111 listbox.querySelectorAll('.cars-fe-occupation-combobox__option.is-active').forEach((li) => li.classList.remove('is-active')); 2112 } 2113 }; 2114 2115 const optionElements = () => [...listbox.querySelectorAll('li.cars-fe-occupation-combobox__option[role="option"]')]; 2116 2117 const highlightActive = () => { 2118 const opts = optionElements(); 2119 opts.forEach((li, i) => li.classList.toggle('is-active', i === activeIdx)); 2120 if (activeIdx >= 0 && opts[activeIdx]) { 2121 query.setAttribute('aria-activedescendant', opts[activeIdx].id); 2122 } else { 2123 query.removeAttribute('aria-activedescendant'); 2124 } 2125 }; 2126 2127 const selectLabel = (label) => { 2128 hidden.value = label; 2129 query.value = label; 2130 setExpanded(false); 2131 notify(); 2132 }; 2133 2134 const tryCommitExactMatch = () => { 2135 const t = query.value.trim(); 2136 if (!t) { 2137 hidden.value = ''; 2138 notify(); 2139 return; 2140 } 2141 const exact = FINANCE_ELIGIBILITY_OCCUPATIONS.find((o) => o.toLowerCase() === t.toLowerCase()); 2142 if (exact) { 2143 hidden.value = exact; 2144 query.value = exact; 2145 } else { 2146 hidden.value = ''; 2147 } 2148 notify(); 2149 }; 2150 2151 const renderList = (items) => { 2152 listbox.textContent = ''; 2153 activeIdx = -1; 2154 if (items.length === 0) { 2155 const li = document.createElement('li'); 2156 li.className = 'cars-fe-occupation-combobox__empty'; 2157 li.setAttribute('role', 'presentation'); 2158 li.textContent = 'No matches'; 2159 listbox.appendChild(li); 2160 return; 2161 }
2162 items.forEach((label, i) => { 2163 const li = document.createElement('li'); 2164 li.id = `${config.listboxId}-opt-${i}`; 2165 li.className = 'cars-fe-occupation-combobox__option'; 2166 li.setAttribute('role', 'option'); 2167 li.dataset.value = label; 2168 li.textContent = label; 2169 li.addEventListener('mousedown', (e) => { 2170 e.preventDefault(); 2171 selectLabel(label); 2172 }); 2173 listbox.appendChild(li); 2174 }); 2175 }; 2176 2177 const openAndRender = () => { 2178 const items = financeEligibilityFilterOccupations(query.value); 2179 renderList(items); 2180 setExpanded(true); 2181 window.requestAnimationFrame(() => { 2182 window.requestAnimationFrame(() => { 2183 financeEligibilityScrollOccupationDropdownIntoView(query, panel); 2184 }); 2185 }); 2186 }; 2187 2188 query.addEventListener('input', () => { 2189 const locked = hidden.value; 2190 if (locked && query.value !== locked) { 2191 hidden.value = ''; 2192 } 2193 openAndRender(); 2194 notify(); 2195 }); 2196 2197 query.addEventListener('focus', () => { 2198 openAndRender(); 2199 }); 2200 2201 query.addEventListener('mousedown', () => { 2202 if (panel.hidden) { 2203 openAndRender(); 2204 } 2205 }); 2206 2207 query.addEventListener('blur', () => { 2208 window.setTimeout(() => { 2209 tryCommitExactMatch(); 2210 setExpanded(false); 2211 }, 180); 2212 }); 2213 2214 query.addEventListener('keydown', (e) => { 2215 const opts = optionElements(); 2216 if (e.key === 'Escape') { 2217 e.preventDefault(); 2218 setExpanded(false); 2219 return; 2220 } 2221 if (!panel.hidden && opts.length) { 2222 if (e.key === 'ArrowDown') { 2223 e.preventDefault(); 2224 activeIdx = activeIdx < opts.length - 1 ? activeIdx + 1 : 0; 2225 highlightActive(); 2226 opts[activeIdx]?.scrollIntoView({ block: 'nearest' }); 2227 } else if (e.key === 'ArrowUp') { 2228 e.preventDefault(); 2229 activeIdx = activeIdx > 0 ? activeIdx - 1 : opts.length - 1; 2230 highlightActive(); 2231 opts[activeIdx]?.scrollIntoView({ block: 'nearest' }); 2232 } else if (e.key === 'Enter') { 2233 if (activeIdx >= 0 && opts[activeIdx]) { 2234 e.preventDefault(); 2235 selectLabel(opts[activeIdx].dataset.value || ''); 2236 } 2237 } 2238 } 2239 }); 2240} 2241 2242function financeEligibilityInitOccupationComboboxes(onChange) { 2243 [ 2244 { 2245 hiddenId: 'cars-fe-employment-occupation', 2246 queryId: 'cars-fe-employment-occupation-q', 2247 listboxId: 'cars-fe-employment-occupation-listbox', 2248 }, 2249 ].forEach((cfg) => financeEligibilityAttachOccupationCombobox(cfg, onChange)); 2250} 2251 2252/** Months preselect 0 so users only change them for a part-year duration; years stays a required choice. */ 2253const FINANCE_ELIGIBILITY_FIELD_DEFAULTS = { 2254 'cars-fe-address-months-at': '0', 2255 'cars-fe-employment-months': '0', 2256}; 2257 2258function financeEligibilityFieldDefaultValue(fieldId) { 2259 return FINANCE_ELIGIBILITY_FIELD_DEFAULTS[fieldId] || ''; 2260} 2261 2262function financeEligibilitySyncDurationListboxShell(shellOrField) { 2263 const shell = 2264 shellOrField?.classList?.contains('cars-fe-field-group__shell--duration') 2265 ? shellOrField 2266 : shellOrField?.closest?.('.cars-fe-field-group__shell--duration'); 2267 if (!shell) { 2268 return; 2269 } 2270 const hidden = shell.querySelector('input[type="hidden"]'); 2271 const display = shell.querySelector('.cars-fe-duration-listbox__display'); 2272 const unitHint = shell.getAttribute('data-fe-duration-unit') || ''; 2273 const raw = String(hidden?.value || '').trim(); 2274 const hasValue = Boolean(raw); 2275 shell.classList.toggle('cars-fe-field-group__shell--duration-has-value', hasValue); 2276 if (display) { 2277 if (!hasValue) { 2278 display.textContent = unitHint; 2279 } else { 2280 const match = [...shell.querySelectorAll('.cars-fe-duration-listbox__option')].find( 2281 (opt) => opt.getAttribute('data-value') === raw, 2282 ); 2283 display.textContent = match?.getAttribute('data-label') || match?.textContent || raw; 2284 } 2285 } 2286} 2287
2288function financeEligibilityCloseDurationListboxes(rootEl, exceptPanel) { 2289 const root = rootEl || document.getElementById('financeEligibilityOffcanvas'); 2290 if (!root) { 2291 return; 2292 } 2293 root.querySelectorAll('.cars-fe-duration-listbox__panel').forEach((panel) => { 2294 if (panel.hidden || panel === exceptPanel) { 2295 return; 2296 } 2297 panel.hidden = true; 2298 const wrap = panel.closest('.cars-fe-duration-listbox'); 2299 const trigger = wrap?.querySelector('.cars-fe-duration-listbox__trigger'); 2300 if (trigger) { 2301 trigger.setAttribute('aria-expanded', 'false'); 2302 trigger.removeAttribute('aria-activedescendant'); 2303 } 2304 wrap?.querySelectorAll('.cars-fe-duration-listbox__option.is-active').forEach((li) => li.classList.remove('is-active')); 2305 }); 2306 const anyOpen = [...root.querySelectorAll('.cars-fe-duration-listbox__panel')].some((p) => !p.hidden); 2307 const employmentHost = 2308 document.querySelector('.cars-fe-form-stack--employment')?.closest('.cars-fe-journey-step') || 2309 document.getElementById('cars-fe-step-employment'); 2310 const steps = [document.getElementById('cars-fe-step-address'), employmentHost].filter(Boolean); 2311 steps.forEach((step) => { 2312 if (rootEl && !root.contains(step)) { 2313 return; 2314 } 2315 if (anyOpen && step.contains(exceptPanel)) { 2316 return; 2317 } 2318 if (!anyOpen || !step.querySelector('.cars-fe-duration-listbox__panel:not([hidden])')) { 2319 step.classList.remove('cars-fe-duration-dropdown-open'); 2320 const stack = 2321 step.querySelector('.cars-fe-form-stack--address') || 2322 step.querySelector('.cars-fe-form-stack--employment') || 2323 step.querySelector('.cars-fe-form-stack'); 2324 if (stack) { 2325 delete stack.dataset.feScrollPadExtra; 2326 stack.style.removeProperty('padding-bottom'); 2327 } 2328 } 2329 }); 2330} 2331 2332function financeEligibilityResolveDurationStep(stepId) { 2333 if (stepId === 'cars-fe-step-employment') { 2334 return ( 2335 document.querySelector('.cars-fe-form-stack--employment')?.closest('.cars-fe-journey-step') || 2336 document.getElementById('cars-fe-step-employment') 2337 ); 2338 } 2339 return document.getElementById(stepId); 2340} 2341 2342function financeEligibilitySetDurationDropdownOpen(stepId, open) { 2343 const step = financeEligibilityResolveDurationStep(stepId); 2344 if (!step) { 2345 return; 2346 } 2347 step.classList.toggle('cars-fe-duration-dropdown-open', Boolean(open)); 2348 if (!open) { 2349 const stack = 2350 step.querySelector('.cars-fe-form-stack--address') || 2351 step.querySelector('.cars-fe-form-stack--employment') || 2352 step.querySelector('.cars-fe-form-stack'); 2353 if (stack) { 2354 delete stack.dataset.feScrollPadExtra; 2355 stack.style.removeProperty('padding-bottom'); 2356 } 2357 } 2358} 2359 2360/** 2361 * Custom years/months listbox (occupation-style panel, no search). 2362 * @param {{ hiddenId: string; listboxId: string; stepId: string }} config 2363 * @param {() => void} [onChange] 2364 */ 2365function financeEligibilityAttachDurationListbox(config, onChange) { 2366 const hidden = document.getElementById(config.hiddenId); 2367 const listbox = document.getElementById(config.listboxId); 2368 const shell = hidden?.closest?.('.cars-fe-field-group__shell--duration'); 2369 const wrap = shell?.querySelector('.cars-fe-duration-listbox'); 2370 const trigger = wrap?.querySelector('.cars-fe-duration-listbox__trigger'); 2371 const panel = wrap?.querySelector('.cars-fe-duration-listbox__panel'); 2372 if (!hidden || !listbox || !trigger || !panel || hidden.dataset.feDurationInit === '1') { 2373 return; 2374 } 2375 hidden.dataset.feDurationInit = '1'; 2376 2377 let activeIdx = -1; 2378 2379 const notify = () => { 2380 financeEligibilitySyncDurationListboxShell(shell); 2381 hidden.dispatchEvent(new Event('change', { bubbles: true })); 2382 if (typeof onChange === 'function') { 2383 onChange(); 2384 } 2385 }; 2386 2387 const optionElements = () => [...listbox.querySelectorAll('li.cars-fe-duration-listbox__option[role="option"]')]; 2388 2389 const setExpanded = (open) => { 2390 if (open) { 2391 financeEligibilityCloseDurationListboxes(null, panel); 2392 } 2393 trigger.setAttribute('aria-expanded', open ? 'true' : 'false'); 2394 panel.hidden = !open; 2395 financeEligibilitySetDurationDropdownOpen(config.stepId, open); 2396 if (!open) { 2397 activeIdx = -1; 2398 trigger.removeAttribute('aria-activedescendant');
2399 optionElements().forEach((li) => li.classList.remove('is-active')); 2400 } 2401 }; 2402 2403 const highlightActive = () => { 2404 const opts = optionElements(); 2405 opts.forEach((li, i) => li.classList.toggle('is-active', i === activeIdx)); 2406 if (activeIdx >= 0 && opts[activeIdx]) { 2407 trigger.setAttribute('aria-activedescendant', opts[activeIdx].id); 2408 } else { 2409 trigger.removeAttribute('aria-activedescendant'); 2410 } 2411 }; 2412 2413 const selectValue = (value) => { 2414 hidden.value = value == null ? '' : String(value); 2415 setExpanded(false); 2416 notify(); 2417 }; 2418 2419 optionElements().forEach((li) => { 2420 li.addEventListener('mousedown', (e) => { 2421 e.preventDefault(); 2422 e.stopPropagation(); 2423 selectValue(li.getAttribute('data-value') || li.textContent || ''); 2424 }); 2425 }); 2426 2427 const openPanel = () => { 2428 if (trigger.disabled || shell.classList.contains('cars-fe-field-group__shell--duration-disabled')) { 2429 return; 2430 } 2431 setExpanded(true); 2432 // Single auto scroll after layout â nested smooth scrolls caused years/months flicker. 2433 window.requestAnimationFrame(() => { 2434 financeEligibilityScrollFieldIntoView(trigger, { 2435 listPanelEl: panel, 2436 topPaddingPx: 8, 2437 gapPx: 16, 2438 behavior: 'auto', 2439 }); 2440 }); 2441 }; 2442 2443 trigger.addEventListener('mousedown', (e) => { 2444 // Prevent focus-driven scroll/close races that force a second click on desktop. 2445 e.preventDefault(); 2446 e.stopPropagation(); 2447 if (panel.hidden) { 2448 openPanel(); 2449 trigger.focus({ preventScroll: true }); 2450 } else { 2451 setExpanded(false); 2452 } 2453 }); 2454 2455 trigger.addEventListener('click', (e) => { 2456 e.preventDefault(); 2457 e.stopPropagation(); 2458 }); 2459 2460 trigger.addEventListener('keydown', (e) => { 2461 const opts = optionElements(); 2462 if (e.key === 'Escape') { 2463 e.preventDefault(); 2464 setExpanded(false); 2465 return; 2466 } 2467 if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && panel.hidden) { 2468 e.preventDefault(); 2469 openPanel(); 2470 return; 2471 } 2472 if (!panel.hidden && opts.length) { 2473 if (e.key === 'ArrowDown') { 2474 e.preventDefault(); 2475 activeIdx = activeIdx < opts.length - 1 ? activeIdx + 1 : 0; 2476 highlightActive(); 2477 opts[activeIdx]?.scrollIntoView({ block: 'nearest' }); 2478 } else if (e.key === 'ArrowUp') { 2479 e.preventDefault(); 2480 activeIdx = activeIdx > 0 ? activeIdx - 1 : opts.length - 1; 2481 highlightActive(); 2482 opts[activeIdx]?.scrollIntoView({ block: 'nearest' }); 2483 } else if (e.key === 'Enter' || e.key === ' ') { 2484 e.preventDefault(); 2485 if (activeIdx >= 0 && opts[activeIdx]) { 2486 selectValue(opts[activeIdx].getAttribute('data-value') || opts[activeIdx].textContent || ''); 2487 } 2488 } 2489 } 2490 }); 2491 2492 financeEligibilitySyncDurationListboxShell(shell); 2493} 2494 2495function financeEligibilityInitDurationListboxes(onChange) { 2496 [ 2497 { 2498 hiddenId: 'cars-fe-address-years-at', 2499 listboxId: 'cars-fe-address-years-at-listbox', 2500 stepId: 'cars-fe-step-address', 2501 }, 2502 { 2503 hiddenId: 'cars-fe-address-months-at', 2504 listboxId: 'cars-fe-address-months-at-listbox', 2505 stepId: 'cars-fe-step-address', 2506 }, 2507 { 2508 hiddenId: 'cars-fe-employment-years', 2509 listboxId: 'cars-fe-employment-years-listbox', 2510 stepId: 'cars-fe-step-employment', 2511 }, 2512 { 2513 hiddenId: 'cars-fe-employment-months', 2514 listboxId: 'cars-fe-employment-months-listbox', 2515 stepId: 'cars-fe-step-employment', 2516 }, 2517 ].forEach((cfg) => financeEligibilityAttachDurationListbox(cfg, onChange)); 2518} 2519 2520function setupCarsFinanceEligibilityPanel() { 2521 const panel = document.getElementById('financeEligibilityOffcanvas'); 2522 if (!panel) { 2523 return; 2524 } 2525 2526 /** Ivendi funder codes â legal entity names for review-step consent copy. */ 2527 const FUNDER_CODE_LENDER_NAMES = Object.freeze({ 2528 BLA: 'Black Horse Limited', 2529 STL: 'Startline Motor Finance', 2530 MOT: 'MotoNovo Finance', 2531 FIA: 'CA Auto Finance UK Ltd', 2532 }); 2533 2534 const resolveFinanceEligibilityLenderDisplayName = (funderCode, productName) => { 2535 const code = String(funderCode || '').trim().toUpperCase(); 2536 if (code && FUNDER_CODE_LENDER_NAMES[code]) { 2537 return FUNDER_CODE_LENDER_NAMES[code]; 2538 } 2539 const product = String(productName || '').trim(); 2540 if (product && /black\s*horse/i.test(product)) { 2541 return 'Black Horse Limited'; 2542 } 2543 if (code) { 2544 return code; 2545 } 2546 return 'Black Horse Limited'; 2547 }; 2548 2549 const getActiveVdpQuoteLenderMetaFromTab = (tab) => { 2550 if (!tab) { 2551 return null; 2552 } 2553 const quoteKey = 2554 String(tab.getAttribute('currentTab') || 'HP').trim().toUpperCase() === 'PCP' ? 'pcp' : 'hp'; 2555 return { 2556 funderCode: tab.getAttribute(`data-${quoteKey}-funder-code`) || '', 2557 productName: tab.getAttribute(`data-${quoteKey}-product-name`) || '', 2558 }; 2559 }; 2560 2561 const readFinancePageQuoteLenderMeta = () => { 2562 const raw = panel.getAttribute('data-fe-finance-page-quote'); 2563 if (!raw) { 2564 return null; 2565 } 2566 try { 2567 const snap = JSON.parse(raw); 2568 return { 2569 funderCode: snap.funderCode || '', 2570 productName: snap.productName || '', 2571 }; 2572 } catch { 2573 return null; 2574 } 2575 }; 2576 2577 const syncReviewConsentLenderName = () => { 2578 const lenderEl = document.getElementById('cars-fe-review-consent-lender-name'); 2579 if (!lenderEl) { 2580 return; 2581 } 2582 const financeMeta = readFinancePageQuoteLenderMeta(); 2583 const vdpMeta = getActiveVdpQuoteLenderMetaFromTab(document.querySelector('.tab-content-container')); 2584 let meta = vdpMeta?.funderCode || vdpMeta?.productName ? vdpMeta : financeMeta; 2585 if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true' && (financeMeta?.funderCode || financeMeta?.productName)) { 2586 meta = financeMeta; 2587 } 2588 lenderEl.textContent = resolveFinanceEligibilityLenderDisplayName( 2589 meta?.funderCode, 2590 meta?.productName, 2591 ); 2592 }; 2593 2594 /** Quote UIDs for finance scan: full list from get-finance-quote, else PCP + HP on the tab. */ 2595 const getVdpFinanceQuoteIdsFromTabContainer = (tabEl) => { 2596 if (!tabEl) { 2597 return []; 2598 } 2599 try { 2600 const raw = tabEl.getAttribute('data-fe-eligibility-quote-ids'); 2601 if (raw) { 2602 const parsed = JSON.parse(raw); 2603 if (Array.isArray(parsed)) { 2604 const fromApi = parsed 2605 .filter((x) => typeof x === 'string' && x.trim().length > 0) 2606 .map((x) => x.trim()); 2607 if (fromApi.length > 0) { 2608 return fromApi; 2609 } 2610 } 2611 } 2612 } catch { 2613 /* ignore */ 2614 } 2615 const ids = []; 2616 ['pcpQuoteUID', 'hpQuoteUID'].forEach((attr) => { 2617 const v = tabEl.getAttribute(attr); 2618 if (v && String(v).trim().length > 0) { 2619 const t = String(v).trim(); 2620 if (!ids.includes(t)) { 2621 ids.push(t); 2622 } 2623 } 2624 }); 2625 return ids; 2626 }; 2627 2628 /** When âCheck eligibilityâ opens the offcanvas, copy live quote UIDs, term, and dealer from the VDP finance calculator. */ 2629 const syncFinanceEligibilityPanelIvendiFromVdp = () => {
2630 // Do not overwrite quote IDs/term bound from a listing/special-offer CTA for another stock. 2631 if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true') { 2632 syncReviewConsentLenderName(); 2633 return; 2634 } 2635 const tab = document.querySelector('.tab-content-container'); 2636 if (tab) { 2637 const quoteIds = getVdpFinanceQuoteIdsFromTabContainer(tab); 2638 if (quoteIds.length > 0) { 2639 panel.setAttribute('data-fe-finance-eligibility-quote-ids', JSON.stringify(quoteIds)); 2640 } else { 2641 panel.removeAttribute('data-fe-finance-eligibility-quote-ids'); 2642 } 2643 const currentTab = tab.getAttribute('currentTab'); 2644 const termAttr = 2645 currentTab === 'PCP' ? tab.getAttribute('data-pcp-term') : tab.getAttribute('data-hp-term'); 2646 const termN = termAttr != null ? parseInt(termAttr, 10) : NaN; 2647 if (!Number.isNaN(termN) && termN > 0) { 2648 panel.setAttribute('data-fe-finance-term', String(termN)); 2649 } 2650 } 2651 syncReviewConsentLenderName(); 2652 }; 2653 2654 const syncFinanceDurationSelectShell = (fieldEl) => { 2655 financeEligibilitySyncDurationListboxShell(fieldEl); 2656 }; 2657 2658 const syncAllFinanceDurationSelectShells = () => { 2659 panel.querySelectorAll('.cars-fe-field-group__shell--duration').forEach(financeEligibilitySyncDurationListboxShell); 2660 }; 2661 2662 const intro = document.getElementById('finance-eligibility-step-intro'); 2663 const agreementView = document.getElementById('finance-eligibility-step-agreement'); 2664 const detailsStep = document.getElementById('cars-fe-step-details'); 2665 const contactStep = document.getElementById('cars-fe-step-contact'); 2666 const moreStep = document.getElementById('cars-fe-step-more'); 2667 const personalGroupedStep = document.getElementById('cars-fe-step-personal-grouped'); 2668 const addressStep = document.getElementById('cars-fe-step-address'); 2669 const employmentStep = document.getElementById('cars-fe-step-employment'); 2670 const incomeOutgoingsStep = document.getElementById('cars-fe-step-income-outgoings'); 2671 const outgoingsFinalStep = document.getElementById('cars-fe-step-outgoings-final'); 2672 const incomeOutgoingsGroupedStep = document.getElementById('cars-fe-step-income-outgoings-grouped'); 2673 const reviewStep = document.getElementById('cars-fe-step-review'); 2674 const nextBtn = document.getElementById('cars-fe-details-next-button'); 2675 const contactNextBtn = document.getElementById('cars-fe-contact-next-button'); 2676 const moreNextBtn = document.getElementById('cars-fe-more-next-button'); 2677 const personalGroupedNextBtn = document.getElementById('cars-fe-personal-grouped-next-button'); 2678 const addressNextBtn = document.getElementById('cars-fe-address-next-button'); 2679 const employmentNextBtn = document.getElementById('cars-fe-employment-next-button'); 2680 const incomeNextBtn = document.getElementById('cars-fe-income-next-button'); 2681 const outgoingsSummaryBtn = document.getElementById('cars-fe-outgoings-final-summary-button'); 2682 const incomeOutgoingsGroupedSummaryBtn = document.getElementById( 2683 'cars-fe-income-outgoings-grouped-summary-button', 2684 ); 2685 const reviewViewResultsBtn = document.getElementById('cars-fe-review-view-results-button'); 2686 const reviewConsentCb = document.getElementById('cars-fe-review-consent-checkbox'); 2687 const resultsModal = document.getElementById('cars-fe-results-modal'); 2688 const resultsCardsMount = document.getElementById('cars-fe-results-cards-mount'); 2689 const resultsModalStatus = document.getElementById('cars-fe-results-modal-status'); 2690 const resultsSortSelect = document.getElementById('cars-fe-results-sort'); 2691 2692 let feFormDensityComposed = false; 2693 2694 const isGroupedFormActive = () => 2695 typeof window.isFinanceEligibilityGroupedFormVariantActive === 'function' && 2696 window.isFinanceEligibilityGroupedFormVariantActive(); 2697 2698 const isControlMicroStepScreen = () => 2699 state.screen === 'details' || 2700 state.screen === 'contact' || 2701 state.screen === 'more' || 2702 state.screen === 'address' || 2703 state.screen === 'employment' || 2704 state.screen === 'incomeOutgoings' || 2705 state.screen === 'outgoingsFinal' || 2706 state.screen === 'review'; 2707 2708 const isGroupedJourneyScreen = () => 2709 state.screen === 'personalGrouped' || state.screen === 'incomeOutgoingsGrouped'; 2710 2711 const getEmploymentUiHost = () => { 2712 if (feFormDensityComposed && isGroupedFormActive() && incomeOutgoingsGroupedStep) { 2713 return incomeOutgoingsGroupedStep; 2714 } 2715 return employmentStep; 2716 }; 2717 2718 const fieldGroupForId = (id) => document.getElementById(id)?.closest('.cars-fe-field-group'); 2719 2720 const moveEl = (el, parent) => { 2721 if (el && parent) { 2722 parent.appendChild(el); 2723 } 2724 }; 2725 2726 const syncIntroStartButtonLabel = () => { 2727 if (!panel) { 2728 return; 2729 }
2730 const grouped = panel.dataset.feFormDensity === 'grouped'; 2731 const applyLabel = (el, groupedAttr, controlAttr) => { 2732 if (!el) { 2733 return; 2734 } 2735 const label = el.getAttribute(grouped ? groupedAttr : controlAttr); 2736 if (label) { 2737 el.textContent = label; 2738 } 2739 }; 2740 2741 applyLabel( 2742 document.getElementById('finance-eligibility-start-button'), 2743 'data-fe-cta-grouped', 2744 'data-fe-cta-control', 2745 ); 2746 // Results screen copy (Figma 40003936:37009 for grouped). Only the success line varies by 2747 // variant: while the results screen shows its empty or error state, syncResultsModalEmptyState 2748 // owns this element, and rewriting it here would replace "No finance products matched" or the 2749 // error copy with the success copy. 2750 if (!document.getElementById('cars-fe-results-modal-empty')) { 2751 applyLabel( 2752 panel.querySelector('.cars-fe-results-modal__intro-sub'), 2753 'data-fe-results-sub-grouped', 2754 'data-fe-results-sub-control', 2755 ); 2756 } 2757 applyLabel( 2758 document.getElementById('cars-fe-results-book-appointment'), 2759 'data-fe-results-cta-grouped', 2760 'data-fe-results-cta-control', 2761 ); 2762 }; 2763 2764 /** 2765 * Every side-panel variant names the destination on the CTA that ends a section, so those 2766 * footers spell the next section out instead of reading "Next". The steps inside a section 2767 * (employment, income and outgoings) keep the plain "Next" from the markup. 2768 */ 2769 const FE_SECTION_END_CTA_LABELS = Object.freeze({ 2770 'cars-fe-more-next-button': 'Continue to Address Details', 2771 'cars-fe-personal-grouped-next-button': 'Continue to Address Details', 2772 'cars-fe-address-next-button': 'Continue to Income and Outgoings', 2773 }); 2774 2775 const FE_SECTION_END_CTA_VARIANTS = Object.freeze(['test-a', 'test-b', 'test-c']); 2776 2777 const syncSectionEndCtaLabels = () => { 2778 const variant = String(window.__checkEligibilityJourneyVariant || '').trim().toLowerCase(); 2779 const useSectionCopy = FE_SECTION_END_CTA_VARIANTS.includes(variant); 2780 Object.keys(FE_SECTION_END_CTA_LABELS).forEach((id) => { 2781 const btn = document.getElementById(id); 2782 if (!btn) { 2783 return; 2784 } 2785 btn.textContent = useSectionCopy ? FE_SECTION_END_CTA_LABELS[id] : 'Next'; 2786 }); 2787 }; 2788 2789 const restoreControlFormDomIfComposed = () => { 2790 if (!feFormDensityComposed) { 2791 return false; 2792 } 2793 2794 const detailsStack = detailsStep?.querySelector('.cars-fe-scroll > .cars-fe-form-stack'); 2795 const contactStack = contactStep?.querySelector('.cars-fe-form-stack--contact'); 2796 const moreStack = moreStep?.querySelector('.cars-fe-form-stack--more'); 2797 const employmentScroll = employmentStep?.querySelector('.cars-fe-scroll'); 2798 const incomeScroll = incomeOutgoingsStep?.querySelector('.cars-fe-scroll'); 2799 const outgoingsScroll = outgoingsFinalStep?.querySelector('.cars-fe-scroll'); 2800 2801 moveEl(fieldGroupForId('cars-fe-title'), detailsStack); 2802 moveEl(fieldGroupForId('cars-fe-first-name'), detailsStack); 2803 moveEl(fieldGroupForId('cars-fe-last-name'), detailsStack); 2804 moveEl(fieldGroupForId('cars-fe-dob'), detailsStack); 2805 moveEl(fieldGroupForId('cars-fe-marital'), detailsStack); 2806 moveEl(fieldGroupForId('cars-fe-email'), contactStack); 2807 moveEl(fieldGroupForId('cars-fe-phone-national'), contactStack); 2808 moveEl(fieldGroupForId('cars-fe-license-type'), moreStack); 2809 moveEl(fieldGroupForId('cars-fe-dependants'), moreStack); 2810 2811 const employmentStack = document.querySelector('.cars-fe-form-stack--employment'); 2812 const incomeStack = document.querySelector('.cars-fe-form-stack--income-outgoings'); 2813 const outgoingsStack = document.querySelector('.cars-fe-form-stack--outgoings-final'); 2814 moveEl(employmentStack, employmentScroll); 2815 moveEl(incomeStack, incomeScroll); 2816 moveEl(outgoingsStack, outgoingsScroll); 2817 2818 const emailInput = document.getElementById('cars-fe-email'); 2819 if (emailInput) { 2820 emailInput.placeholder = '[email protected]'; 2821 } 2822 const licenseLabel = fieldGroupForId('cars-fe-license-type')?.querySelector( 2823 '.cars-fe-field-group__label-row .cars-fe-field-group__label, .cars-fe-field-group__label', 2824 ); 2825 if (licenseLabel) { 2826 licenseLabel.textContent = 'Driving License Type'; 2827 } 2828 2829 incomeOutgoingsGroupedStep?.classList.remove( 2830 'cars-fe-employment--has-saved-summary', 2831 'cars-fe-employment--entry-flow-visible', 2832 'cars-fe-occupation-dropdown-open', 2833 'cars-fe-duration-dropdown-open', 2834 ); 2835 personalGroupedStep?.classList.remove( 2836 'cars-fe-occupation-dropdown-open', 2837 'cars-fe-duration-dropdown-open', 2838 ); 2839 2840 feFormDensityComposed = false;
2841 return true; 2842 }; 2843 2844 const ensureGroupedFormDomComposed = () => { 2845 if (feFormDensityComposed || !isGroupedFormActive()) { 2846 return feFormDensityComposed; 2847 } 2848 const personalMount = document.getElementById('cars-fe-personal-grouped-mount'); 2849 const employmentMount = document.getElementById('cars-fe-income-outgoings-grouped-employment-mount'); 2850 const incomeMount = document.getElementById('cars-fe-income-outgoings-grouped-income-mount'); 2851 const outgoingsMount = document.getElementById('cars-fe-income-outgoings-grouped-outgoings-mount'); 2852 if (!personalMount || !employmentMount || !incomeMount || !outgoingsMount) { 2853 return false; 2854 } 2855 2856 // Figma About you order: title, names, DOB, email, phone, marital, licence, dependants. 2857 moveEl(fieldGroupForId('cars-fe-title'), personalMount); 2858 moveEl(fieldGroupForId('cars-fe-first-name'), personalMount); 2859 moveEl(fieldGroupForId('cars-fe-last-name'), personalMount); 2860 moveEl(fieldGroupForId('cars-fe-dob'), personalMount); 2861 moveEl(fieldGroupForId('cars-fe-email'), personalMount); 2862 moveEl(fieldGroupForId('cars-fe-phone-national'), personalMount); 2863 moveEl(fieldGroupForId('cars-fe-marital'), personalMount); 2864 moveEl(fieldGroupForId('cars-fe-license-type'), personalMount); 2865 moveEl(fieldGroupForId('cars-fe-dependants'), personalMount); 2866 2867 const emailInput = document.getElementById('cars-fe-email'); 2868 if (emailInput) { 2869 emailInput.placeholder = 'Email'; 2870 } 2871 const licenseLabel = fieldGroupForId('cars-fe-license-type')?.querySelector( 2872 '.cars-fe-field-group__label-row .cars-fe-field-group__label, .cars-fe-field-group__label', 2873 ); 2874 if (licenseLabel) { 2875 licenseLabel.textContent = 'Driving Licence Type'; 2876 } 2877 2878 // Figma combined screen: Employment + About your income + Outgoings. 2879 const employmentStack = employmentStep?.querySelector('.cars-fe-form-stack--employment'); 2880 const incomeStack = incomeOutgoingsStep?.querySelector('.cars-fe-form-stack--income-outgoings'); 2881 const outgoingsStack = outgoingsFinalStep?.querySelector('.cars-fe-form-stack--outgoings-final'); 2882 moveEl(employmentStack, employmentMount); 2883 moveEl(incomeStack, incomeMount); 2884 moveEl(outgoingsStack, outgoingsMount); 2885 2886 if (panel) { 2887 panel.dataset.feFormDensity = 'grouped'; 2888 } 2889 feFormDensityComposed = true; 2890 return true; 2891 }; 2892 2893 /** Journey variant on the panel so intro markup can be selected in CSS (test-b intro is its own layout). */ 2894 const syncJourneyVariantAttribute = () => { 2895 if (!panel) { 2896 return; 2897 } 2898 const variant = String(window.__checkEligibilityJourneyVariant || 'control').trim().toLowerCase(); 2899 panel.dataset.feJourneyVariant = variant === '' ? 'control' : variant; 2900 }; 2901 2902 const applyFormDensityVariant = () => { 2903 if (!panel) { 2904 return; 2905 } 2906 2907 syncJourneyVariantAttribute(); 2908 syncSectionEndCtaLabels(); 2909 2910 // Never reparent fields while the user is mid-control micro-steps (late PostHog resolve). 2911 if (isControlMicroStepScreen()) { 2912 return; 2913 } 2914 2915 // Stay on grouped DOM for the rest of this open session if already in a grouped step. 2916 if (isGroupedJourneyScreen()) { 2917 panel.dataset.feFormDensity = 'grouped'; 2918 syncIntroStartButtonLabel(); 2919 return; 2920 } 2921 2922 if (isGroupedFormActive()) { 2923 ensureGroupedFormDomComposed(); 2924 panel.dataset.feFormDensity = 'grouped'; 2925 } else { 2926 restoreControlFormDomIfComposed(); 2927 panel.dataset.feFormDensity = 'control'; 2928 } 2929 2930 syncIntroStartButtonLabel(); 2931 }; 2932 2933 /** 2934 * Routing has to follow the DOM, not the flag alone. When PostHog resolves to test-c after the 2935 * user has already started the control micro-steps, applyFormDensityVariant deliberately 2936 * refuses to reparent the fields, so the grouped steps hold nothing. Routing on the flag then 2937 * hid every step and showed an empty panel, or made Next a no-op. Ask for c
2937omposition first and 2938 * only take the grouped route once it has actually happened; otherwise stay on the control 2939 * steps, which is the DOM the user is already looking at. 2940 */ 2941 const canRouteToGroupedSteps = (stepEl) => { 2942 if (!isGroupedFormActive()) { 2943 return false; 2944 } 2945 applyFormDensityVariant(); 2946 return feFormDensityComposed && Boolean(stepEl); 2947 }; 2948 2949 let feScanPollGeneration = 0; 2950 let feScanPollRetryContext = null; 2951 2952 const setFinanceScanLoadingRetryVisible = (visible) => { 2953 const btn = document.getElementById('cars-fe-results-loading-retry'); 2954 if (!btn) { 2955 return; 2956 } 2957 btn.hidden = !visible; 2958 btn.setAttribute('aria-hidden', visible ? 'false' : 'true'); 2959 }; 2960 2961 const FE_ELIGIBILITY_CAT_CLASSNAMES = [ 2962 'cars-fe-eligibility-cat--pre-approved', 2963 'cars-fe-eligibility-cat--high', 2964 'cars-fe-eligibility-cat--amber', 2965 'cars-fe-eligibility-cat--low', 2966 ]; 2967 2968 const FE_ELIGIBILITY_UNKNOWN_LABEL = 'Eligibility unknown'; 2969 2970 /** CSS class keys only â badge text always comes from EligibilityOffered (or unknown). */ 2971 const FE_ELIGIBILITY_CATEGORY_META = { 2972 'pre-approved': { className: 'cars-fe-eligibility-cat--pre-approved' }, 2973 high: { className: 'cars-fe-eligibility-cat--high' }, 2974 'alternative-offer': { className: 'cars-fe-eligibility-cat--amber' }, 2975 medium: { className: 'cars-fe-eligibility-cat--amber' }, 2976 low: { className: 'cars-fe-eligibility-cat--low' }, 2977 unable: { className: 'cars-fe-eligibility-cat--low' }, 2978 unknown: { className: 'cars-fe-eligibility-cat--low' }, 2979 }; 2980 2981 const getFeEligibilityOfferedRaw = (quote) => { 2982 if (!quote || typeof quote !== 'object') { 2983 return ''; 2984 } 2985 const raw = 2986 quote.EligibilityOffered ?? 2987 quote.eligibilityOffered ?? 2988 quote?.LenderData?.Eligibility ?? 2989 quote?.lenderData?.eligibility; 2990 if (raw == null) { 2991 return ''; 2992 } 2993 return String(raw).trim(); 2994 }; 2995 2996 const FE_ELIGIBILITY_BADGE_LABELS = { 2997 unknown: FE_ELIGIBILITY_UNKNOWN_LABEL, 2998 eligibilityunknown: FE_ELIGIBILITY_UNKNOWN_LABEL, 2999 preapproved: 'Pre-Approved', 3000 preapprovedeligibility: 'Pre-Approved', 3001 alternativeoffer: 'Alternative Offer', 3002 alternative: 'Alternative Offer', 3003 alternativeoffers: 'Alternative Offer', 3004 unabletoassist: 'Unable to assist', 3005 unable: 'Unable to assist', 3006 high: 'High likelihood of approval', 3007 higheligibility: 'High likelihood of approval', 3008 highlikelihoodofapproval: 'High likelihood of approval', 3009 medium: 'Medium likelihood of approval', 3010 mediumeligibility: 'Medium likelihood of approval', 3011 low: 'Low likelihood of approval', 3012 loweligibility: 'Low likelihood of approval', 3013 }; 3014 3015 const compactEligibilityOfferedValue = (raw) => 3016 String(raw) 3017 .trim() 3018 .replace(/([a-z0-9])([A-Z])/g, '$1-$2') 3019 .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') 3020 .replace(/[\s_-]+/g, '') 3021 .toLowerCase(); 3022 3023 /** Badge text from EligibilityOffered; null/Unknown â "Eligibility unknown". */ 3024 const getFeEligibilityBadgeLabel = (quote) => { 3025 const raw = getFeEligibilityOfferedRaw(quote); 3026 if (!raw) { 3027 return FE_ELIGIBILITY_UNKNOWN_LABEL; 3028 } 3029 const compact = compactEligibilityOfferedValue(raw); 3030 if (Object.prototype.hasOwnProperty.call(FE_ELIGIBILITY_BADGE_LABELS, compact)) { 3031 return FE_ELIGIBILITY_BADGE_LABELS[compact]; 3032 } 3033 return raw; 3034 }; 3035 3036 const resolveFeEligibilityCategoryKey = (quote) => { 3037 const original = getFeEligibilityOfferedRaw(quote); 3038 if (!original) { 3039 return 'unknown'; 3040 } 3041 // Style hint only â maps API values like UnableToAssist onto colour classes. 3042 const slug = original 3043 .replace(/([a-z0-9])([A-Z])/g, '$1-$2') 3044 .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') 3045 .replace(/[\s_]+/g, '-') 3046 .toLowerCase(); 3047 const compact = slug.replace(/-/g, ''); 3048 const aliases = { 3049 'pre-approved': 'pre-approved', 3050 preapproved: 'pre-approved', 3051 '1': 'pre-approved', 3052 'high-likelihood-of-approval': 'high', 3053 'high-likelihood': 'high', 3054 'high-eligibility': 'high', 3055 high: 'high', 3056 '2': 'high', 3057 'alternative-offer': 'alternative-offer', 3058 alternativeoffer: 'alternative-offer', 3059 alternative: 'alternative-offer', 3060 '3': 'alternative-offer', 3061 'medium-eligibility': 'medium', 3062 medium: 'medium', 3063 '4': 'medium', 3064 'low-eligibility': 'low', 3065 low: 'low', 3066 '5': 'low', 3067 'unable-to-assist': 'unable', 3068 unabletoassist: 'unable', 3069 unable: 'unable', 3070 'not-eligible': 'unable', 3071 noteligible: 'unable', 3072 '6': 'unable', 3073 }; 3074 if (aliases[slug]) { 3075 return aliases[slug]; 3076 } 3077 if (aliases[compact]) { 3078 return aliases[compact]; 3079 } 3080 if (/^pre[-\s]?approved$/i.test(slug) || compact === 'preapproved') { 3081 return 'pre-approved'; 3082 } 3083 if (/alternative/i.test(slug) && /offer/i.test(slug)) { 3084 return 'alternative-offer'; 3085 } 3086 if (/unable/i.test(slug) || /not-?eligible/i.test(slug)) { 3087 return 'unable'; 3088 } 3089 if (/medium/i.test(slug)) { 3090 return 'medium'; 3091 } 3092 if (slug === 'low' || /^low(\s|-|$)/i.test(slug)) { 3093 return 'low'; 3094 } 3095 if (/high/i.test(slug) && /(likelihood|approval|eligibility)/i.test(slug)) { 3096 return 'high'; 3097 } 3098 return 'unknown'; 3099 }; 3100 3101 const syncFeEligibilityCategoryPillClass = (el, categoryKey) => { 3102 if (!el) { 3103 return; 3104 } 3105 FE_ELIGIBILITY_CAT_CLASSNAMES.forEach((c) => el.classList.remove(c)); 3106 const cn = FE_ELIGIBILITY_CATEGORY_META[categoryKey]?.className; 3107 if (cn) { 3108 el.classList.add(cn); 3109 } 3110 }; 3111 3112 const state = { screen: 'intro', dirty: false }; 3113 3114 /** 3115 * URL segments aligned to iVendi Online Finance Navigator funnel labels 3116 * (Landing â Personal details â Address â Income/outgoings â Summary â Consent â Submit â Form submitted â Results). 3117 */ 3118 const getFinanceEligibilityJourneyUrlSegment = () => { 3119 if (resultsModal && resultsModal.hidden === false) { 3120 const loadingLayer = document.getElementById('cars-fe-results-modal-loading'); 3121 if (loadingLayer && loadingLayer.hidden === false) { 3122 return 'form-submitted'; 3123 } 3124 return 'results-page'; 3125 } 3126 if (state.screen === 'intro' || state.screen === 'agreement') { 3127 return 'landing'; 3128 } 3129 if (state.screen === 'details' || state.screen === 'contact' || state.screen === 'more' || state.screen === 'personalGrouped') { 3130 return 'personal-details'; 3131 } 3132 if (state.screen === 'address') {
3133 return 'address-details'; 3134 } 3135 if ( 3136 state.screen === 'employment' || 3137 state.screen === 'incomeOutgoings' || 3138 state.screen === 'outgoingsFinal' || 3139 state.screen === 'incomeOutgoingsGrouped' 3140 ) { 3141 return 'income-outgoings'; 3142 } 3143 if (state.screen === 'review') { 3144 const consentOn = Boolean(reviewConsentCb && reviewConsentCb.checked); 3145 return consentOn ? 'consent' : 'summary'; 3146 } 3147 return 'landing'; 3148 }; 3149 3150 const clearFinanceEligibilityJourneyUrl = () => { 3151 if (typeof history === 'undefined' || !history.replaceState) { 3152 return; 3153 } 3154 const h = window.location.hash || ''; 3155 if (!h.startsWith('#finance-check=')) { 3156 return; 3157 } 3158 history.replaceState(null, '', `${window.location.pathname}${window.location.search}`); 3159 }; 3160 3161 const syncFinanceEligibilityJourneyUrl = () => { 3162 if (typeof history === 'undefined' || !history.replaceState || !panel) { 3163 return; 3164 } 3165 if (!panel.classList.contains('show')) { 3166 return; 3167 } 3168 const segment = getFinanceEligibilityJourneyUrlSegment(); 3169 const base = `${window.location.pathname}${window.location.search}`; 3170 const nextUrl = `${base}#finance-check=${encodeURIComponent(segment)}`; 3171 const current = `${base}${window.location.hash || ''}`; 3172 if (current !== nextUrl) { 3173 history.replaceState(null, '', nextUrl); 3174 } 3175 }; 3176 3177 let addressLookupTimer = null; 3178 let savedAddressEntries = []; 3179 let editingEntryId = null; 3180 let editingInsertIndex = null; 3181 let savedEmploymentEntries = []; 3182 let employmentFormMode = 'editing'; 3183 let editingEmploymentId = null; 3184 let editingEmploymentIndex = null; 3185 let eligibilityQuotesCache = []; 3186 /** When true, completing the current section (Next/Save) returns to Review instead of advancing the journey. */ 3187 let reviewEditReturnPending = false;
3188 /** Removes visualViewport listeners + background scroll lock from show.bs.offcanvas. */ 3189 let financeEligibilityViewportLockCleanup = null; 3190 let financeEligibilityLockedScrollY = 0; 3191 3192 const clearReviewEditReturnPending = () => { 3193 reviewEditReturnPending = false; 3194 }; 3195 3196 const val = (id) => (document.getElementById(id)?.value || '').trim(); 3197 3198 const scrollToTop = (rootEl) => { 3199 if (!rootEl) { 3200 return; 3201 } 3202 const scrollArea = rootEl.querySelector( 3203 '.finance-eligibility-intro__scroll, .finance-eligibility-agreement__scroll, .cars-fe-scroll', 3204 ); 3205 if (scrollArea) { 3206 scrollArea.scrollTop = 0; 3207 } 3208 }; 3209 3210 const setBtnNextDisabled = (btn, disabled) => { 3211 if (!btn) { 3212 return; 3213 } 3214 if (disabled) { 3215 btn.classList.add('cars-fe-next--disabled'); 3216 btn.setAttribute('aria-disabled', 'true'); 3217 } else { 3218 btn.classList.remove('cars-fe-next--disabled'); 3219 btn.removeAttribute('aria-disabled'); 3220 } 3221 }; 3222 3223 const feStepHighlightRequested = { 3224 details: false, 3225 contact: false, 3226 more: false, 3227 address: false, 3228 employment: false, 3229 income: false, 3230 outgoings: false, 3231 }; 3232 3233 /** 3234 * Fields the user has finished with (focusout). A field surfaces its own error once it is 3235 * touched, so problems are visible before Next is pressed â but a field the user is still 3236 * part-way through typing stays unflagged. 3237 */ 3238 const feTouchedFields = new Set(); 3239 3240 /** 3241 * The id of the field the user is typing in right now, for the fields whose errors would 3242 * otherwise fire mid-entry: a part-typed email, and a date of birth whose year is still being 3243 * filled in. Tracked explicitly rather than read from document.activeElement, which during 3244 * focusout can still report the field being left and so holds its error back a beat too long. 3245 */ 3246 let feFieldBeingEdited = null; 3247 3248 const feMarkFieldTouched = (fieldId) => { 3249 if (!fieldId || feTouchedFields.has(fieldId)) { 3250 return false; 3251 } 3252 feTouchedFields.add(fieldId); 3253 return true; 3254 }; 3255 3256 const feForgetTouchedFields = (stepEl) => { 3257 feFieldBeingEdited = null; 3258 if (!stepEl) { 3259 feTouchedFields.clear(); 3260 return; 3261 } 3262 stepEl.querySelectorAll('[id]').forEach((el) => feTouchedFields.delete(el.id)); 3263 }; 3264 3265 const feFieldHighlightAllowed = (stepKey, fieldId) => 3266 feStepHighlightRequested[stepKey] === true || feTouchedFields.has(fieldId); 3267 3268 /** Marks a field invalid only once Next was pressed for its step, or the user has left that field. */ 3269 const feStepFieldMarker = (stepKey) => (fieldId, invalid) => 3270 financeEligibilityMarkFieldInvalid(fieldId, Boolean(invalid) && feFieldHighlightAllowed(stepKey, fieldId)); 3271 3272 const financeEligibilityClearStepInvalidMarks = (stepEl) => { 3273 if (!stepEl) { 3274 return; 3275 } 3276 stepEl.querySelectorAll('.cars-fe-field-group--invalid').forEach((group) => { 3277 group.classList.remove('cars-fe-field-group--invalid'); 3278 }); 3279 const phoneErr = stepEl.querySelector('#cars-fe-phone-format-error'); 3280 if (phoneErr) { 3281 phoneErr.hidden = true; 3282 phoneErr.setAttribute('aria-hidden', 'true'); 3283 } 3284 const manualPcErr = stepEl.querySelector('#cars-fe-address-postcode-manual-error'); 3285 if (manualPcErr) { 3286 manualPcErr.hidden = true; 3287 manualPcErr.setAttribute('aria-hidden', 'true'); 3288 } 3289 }; 3290 3291 const resetFeStepFieldHighlights = (stepEl, stepKey) => { 3292 if (stepKey && Object.prototype.hasOwnProperty.call(feStepHighlightRequested, stepKey)) { 3293 feStepHighlightRequested[stepKey] = false; 3294 } 3295 feForgetTouchedFields(stepEl); 3296 financeEligibilityClearStepInvalidMarks(stepEl); 3297 }; 3298 3299 const financeEligibilityMarkFieldInvalid = (fieldId, invalid) => { 3300 const el = document.getElementById(fieldId); 3301 if (!el) { 3302 return; 3303 } 3304 let group = el.closest('.cars-fe-field-group'); 3305 if (!group && el.closest('.cars-fe-pills')) { 3306 group = el.closest('.cars-fe-pills')?.closest('.cars-fe-field-group') ?? null; 3307 } 3308 if (group) { 3309 group.classList.toggle('cars-fe-field-group--invalid', Boolean(invalid)); 3310 } 3311 }; 3312
3313 const financeEligibilityScrollToFirstInvalid = (stepEl) => { 3314 if (!stepEl) { 3315 return; 3316 } 3317 const first = stepEl.querySelector('.cars-fe-field-group--invalid'); 3318 if (!first) { 3319 return; 3320 } 3321 const focusable = first.querySelector('select, input:not([type="hidden"]), textarea, .cars-fe-occupation-combobox__input'); 3322 financeEligibilityScrollFieldIntoView(focusable || first, { 3323 scrollArea: stepEl.querySelector('.cars-fe-scroll'), 3324 topPaddingPx: 24, 3325 }); 3326 }; 3327 3328 const requestFeStepFieldHighlights = (stepKey) => { 3329 if (Object.prototype.hasOwnProperty.call(feStepHighlightRequested, stepKey)) { 3330 feStepHighlightRequested[stepKey] = true; 3331 } 3332 feFieldBeingEdited = null; 3333 }; 3334 3335 const attemptFeJourneyNext = (stepKey, stepEl, validateFn, onSuccess) => { 3336 requestFeStepFieldHighlights(stepKey); 3337 const ok = validateFn(); 3338 if (!ok) { 3339 financeEligibilityScrollToFirstInvalid(stepEl); 3340 return; 3341 } 3342 resetFeStepFieldHighlights(stepEl, stepKey); 3343 onSuccess(); 3344 }; 3345 3346 const attemptPersonalGroupedNext = (onSuccess) => { 3347 requestFeStepFieldHighlights('details'); 3348 requestFeStepFieldHighlights('contact'); 3349 requestFeStepFieldHighlights('more'); 3350 const ok = validatePersonalGrouped(); 3351 if (!ok) { 3352 financeEligibilityScrollToFirstInvalid(personalGroupedStep); 3353 return; 3354 } 3355 resetFeStepFieldHighlights(personalGroupedStep, 'details'); 3356 feStepHighlightRequested.contact = false; 3357 feStepHighlightRequested.more = false; 3358 onSuccess(); 3359 }; 3360 3361 const attemptIncomeOutgoingsGroupedNext = (onSuccess) => { 3362 requestFeStepFieldHighlights('employment'); 3363 requestFeStepFieldHighlights('income'); 3364 requestFeStepFieldHighlights('outgoings'); 3365 const ok = validateIncomeOutgoingsGrouped(); 3366 if (!ok) { 3367 financeEligibilityScrollToFirstInvalid(incomeOutgoingsGroupedStep); 3368 return; 3369 } 3370 resetFeStepFieldHighlights(incomeOutgoingsGroupedStep, 'employment'); 3371 feStepHighlightRequested.income = false; 3372 feStepHighlightRequested.outgoings = false; 3373 onSuccess(); 3374 }; 3375 3376 const dobIsoOk = (v) => { 3377 const s = String(v || '').trim(); 3378 if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) { 3379 return false; 3380 } 3381 const parts = s.split('-').map((x) => parseInt(x, 10)); 3382 if (parts.length !== 3 || parts.some((n) => Number.isNaN(n))) { 3383 return false; 3384 } 3385 const [y, mo, da] = parts; 3386 const d = new Date(y, mo - 1, da); 3387 if (d.getFullYear() !== y || d.getMonth() !== mo - 1 || d.getDate() !== da) { 3388 return false; 3389 } 3390 const today = new Date(); 3391 today.setHours(0, 0, 0, 0); 3392 d.setHours(0, 0, 0, 0); 3393 let age = today.getFullYear() - d.getFullYear(); 3394 const dm = today.getMonth() - d.getMonth(); 3395 if (dm < 0 || (dm === 0 && today.getDate() < d.getDate())) { 3396 age -= 1; 3397 } 3398 return age >= 18 && age <= 98; 3399 }; 3400 3401 const formatIsoDobToUk = (iso) => { 3402 const s = String(iso || '').trim(); 3403 if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) { 3404 return ''; 3405 } 3406 const [y, m, d] = s.split('-'); 3407 return `${d}/${m}/${y}`; 3408 }; 3409 3410 const EMAIL_LOCAL_PART = /^[A-Za-z0-9_+-]+(?:\.[A-Za-z0-9_+-]+)*$/; 3411 const EMAIL_LOCAL_CHARS = /^[A-Za-z0-9._+-]*$/; 3412 3413 const emailLocalCanStillGrow = (local) => 3414 EMAIL_LOCAL_CHARS.test(local) && local.startsWith('.') === false && local.includes('..') === false; 3415 const EMAIL_DOMAIN_NAME = 3416 /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/; 3417 // Four octets, each 0-255. A partial or out-of-range one (192.168.1, 999.999.999.999) fails. 3418 const EMAIL_DOMAIN_IPV4 = 3419 /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/; 3420 3421 const emailOk = (v) => { 3422 const s = String(v || '').trim(); 3423 const at = s.lastIndexOf('@'); 3424 if (at <= 0 || at === s.length - 1) { 3425 return false;
3426 } 3427 if (!EMAIL_LOCAL_PART.test(s.slice(0, at))) { 3428 return false; 3429 } 3430 const domain = s.slice(at + 1); 3431 return EMAIL_DOMAIN_NAME.test(domain) || EMAIL_DOMAIN_IPV4.test(domain); 3432 }; 3433 3434 const financeEligibilityIsUkPhoneContext = () => { 3435 const iso = String(val('cars-fe-phone-country') || '').toLowerCase(); 3436 const dial = String(val('cars-fe-phone-dial') || '').replace(/\D/g, ''); 3437 return iso === 'gb' || dial === '44'; 3438 }; 3439 3440 const nationalPhoneOk = (v) => { 3441 const raw = String(v || '').trim(); 3442 const compact = raw.replace(/[\s()-]/g, ''); 3443 // Full international: +44 then 10 subscriber digits (12 chars after + including country code digits). 3444 if (/^\+44\d{10}$/.test(compact)) { 3445 return true; 3446 } 3447 const digits = compact.replace(/\D/g, ''); 3448 if (financeEligibilityIsUkPhoneContext()) { 3449 // National: 07xxxxxxxxx (11) or 7xxxxxxxxx (10, dial already +44). 3450 return /^07\d{9}$/.test(digits) || /^7\d{9}$/.test(digits); 3451 } 3452 return /^07\d{9}$/.test(digits) || /^\+44\d{10}$/.test(compact); 3453 }; 3454 3455 /** True once the user has typed more digits than a valid UK mobile allows. */ 3456 const nationalPhoneExceedsLimit = (v) => { 3457 const raw = String(v || '').trim(); 3458 if (!raw) { 3459 return false; 3460 } 3461 const compact = raw.replace(/[\s()-]/g, ''); 3462 if (compact.startsWith('+44')) { 3463 const afterPlus = compact.slice(1).replace(/\D/g, ''); 3464 // +44 + 10 subscriber digits => 12 digits after the + 3465 return afterPlus.length > 12; 3466 } 3467 const digits = compact.replace(/\D/g, ''); 3468 if (financeEligibilityIsUkPhoneContext()) { 3469 // Allow typing toward 07⦠(11) or 7⦠(10); over either max is too long. 3470 if (digits.startsWith('0')) { 3471 return digits.length > 11; 3472 } 3473 return digits.length > 10; 3474 } 3475 return digits.length > 11; 3476 }; 3477 3478 /** True while the digits typed so far are still a prefix of a valid UK number (07⦠/ 7â¦). */ 3479 const ukNationalDigitsPrefixOk = (digits) => { 3480 if (digits === '') { 3481 return true; 3482 } 3483 if (digits.startsWith('0')) { 3484 if (digits.length === 1) { 3485 return true; 3486 } 3487 return digits[1] === '7' && digits.length <= 11; 3488 } 3489 return digits[0] === '7' && digits.length <= 10; 3490 }; 3491 3492 /** 3493 * False once the value can no longer grow into a valid number (e.g. "921" â wrong leading digit). 3494 * Lets a wrong format show its error while typing, without flagging a half-typed valid one. 3495 */ 3496 const nationalPhoneCanStillBecomeValid = (v) => { 3497 const compact = String(v || '').trim().replace(/[\s()-]/g, ''); 3498 if (compact === '') { 3499 return true; 3500 } 3501 if (compact.startsWith('+')) { 3502 // Typing toward +44 then 10 subscriber digits. 3503 return /^\+(4(4\d{0,10})?)?$/.test(compact); 3504 } 3505 const digits = compact.replace(/\D/g, ''); 3506 if (digits !== compact) { 3507 return false; 3508 } 3509 if (financeEligibilityIsUkPhoneContext()) { 3510 return ukNationalDigitsPrefixOk(digits); 3511 } 3512 // Non-UK dial context still expects 07⦠locally. 3513 return digits.startsWith('0') ? ukNationalDigitsPrefixOk(digits) : digits[0] === '0'; 3514 }; 3515 3516 const emailLooksComplete = (v) => { 3517 const s = String(v || '').trim(); 3518 const at = s.indexOf('@'); 3519 if (at <= 0 || (s.match(/@/g) || []).length !== 1) { 3520 return false; 3521 } 3522 const labels = s.slice(at + 1).split('.'); 3523 if (labels.length < 2) { 3524 return false; 3525 } 3526 3527 if (/^[A-Za-z]{2,}$/.test(labels[labels.length - 1])) { 3528 return true; 3529 } 3530 3531 // An all-numeric domain can only be an IPv4 address, so it is judged as soon as it has a dot 3532 // and a non-empty last label. That flags "[email protected]" and "[email protected]" as they are typed; a 3533 // full address passes on its fourth octet. A numeral inside a name is unaffected, since 3534 // "[email protected]" is judged by the branch above and passes. 3535 return labels.every((label) => /^\d+$/.test(label)); 3536 }; 3537 3538 /** Live check: only rejects what no further typing could rescue, so a part-typed address waits. */ 3539 const emailCanStillBecomeValid = (v) => { 3540 const s = String(v || ''); 3541 if (s === '') { 3542 return true; 3543 } 3544 if (/\s/.test(s) || (s.match(/@/g) || []).length > 1) { 3545 return false;
3546 } 3547 const at = s.indexOf('@'); 3548 const local = at === -1 ? s : s.slice(0, at); 3549 // Once the @ is there the local part can no longer grow, so it is held to the full rule. 3550 const localIsFinished = at !== -1; 3551 if (local !== '' && !(localIsFinished ? EMAIL_LOCAL_PART.test(local) : emailLocalCanStillGrow(local))) { 3552 return false; 3553 } 3554 if (at === -1) { 3555 return true; 3556 } 3557 // The domain is still growing, so only what it can never contain is rejected here. 3558 const domain = s.slice(at + 1); 3559 if (domain === '') { 3560 return true; 3561 } 3562 return /^[A-Za-z0-9.-]+$/.test(domain) && domain.startsWith('.') === false && domain.includes('..') === false; 3563 }; 3564 3565 const syncFieldInlineError = (errorId, invalid) => { 3566 const err = document.getElementById(errorId); 3567 if (!err) { 3568 return; 3569 } 3570 const show = Boolean(invalid); 3571 err.hidden = !show; 3572 err.setAttribute('aria-hidden', show ? 'false' : 'true'); 3573 }; 3574 3575 const syncPhoneFormatError = (invalid) => syncFieldInlineError('cars-fe-phone-format-error', invalid); 3576 3577 const syncEmailFormatError = (invalid) => syncFieldInlineError('cars-fe-email-format-error', invalid); 3578 3579 /** 3580 * A date input reports a complete value as soon as every segment holds something, so a year 3581 * typed as 1, 19, 199 reads as 0001, 0019, 0199 and each of those is a real date far outside 3582 * the age range. Waiting for all four digits keeps the error from flashing mid-year. 3583 */ 3584 const dobYearIsComplete = (iso) => { 3585 const parsed = /^(\d{4})-\d{2}-\d{2}$/.exec(String(iso || '').trim()); 3586 return parsed ? parseInt(parsed[1], 10) >= 1000 : false; 3587 }; 3588 3589 /** Part-way through the year is not a wrong date, so neither the error nor the red shows yet. */ 3590 /** 3591 * A wrong date is called out as soon as the year reads as finished, so picking one from the 3592 * calendar flags straight away. A year still being typed (1, then 19, then 199 on the way to 3593 * 1990) is left alone until the user moves off the field, at which point whatever is there is 3594 * final and is judged in full â including a year like 0001 that was abandoned part-typed. 3595 */ 3596 const dobJudgeable = (iso) => dobYearIsComplete(iso) || feFieldBeingEdited !== 'cars-fe-dob'; 3597 3598 const syncDobAgeError = () => { 3599 const dob = val('cars-fe-dob'); 3600 const hasValue = String(dob || '').trim() !== ''; 3601 syncFieldInlineError('cars-fe-dob-age-error', hasValue && dobJudgeable(dob) && !dobIsoOk(dob)); 3602 }; 3603 3604 /** Single source of truth for the phone field, shared by the live-typing and Next paths. */ 3605 const contactPhoneFeedbackState = () => { 3606 const phoneRaw = val('cars-fe-phone-national'); 3607 const trimmed = String(phoneRaw || '').trim(); 3608 const overLimit = nationalPhoneExceedsLimit(phoneRaw); 3609 const invalid = !nationalPhoneOk(phoneRaw); 3610 // A value that can never become valid is shown while typing; a half-typed valid one 3611 // waits until the field is touched or Next is pressed. 3612 const wrongFormat = trimmed !== '' && !nationalPhoneCanStillBecomeValid(phoneRaw); 3613 const live = overLimit || wrongFormat; 3614 const gated = feFieldHighlightAllowed('contact', 'cars-fe-phone-national'); 3615 return { 3616 markInvalid: live || (invalid && gated), 3617 showFormatError: live || (invalid && trimmed !== '' && gated), 3618 }; 3619 }; 3620 3621 const syncLivePhoneFormatFeedback = () => { 3622 const phone = contactPhoneFeedbackState(); 3623 financeEligibilityMarkFieldInvalid('cars-fe-phone-national', phone.markInvalid); 3624 syncPhoneFormatError(phone.showFormatError); 3625 }; 3626 3627 const syncManualUkPostcodeError = (invalid) => { 3628 const err = document.getElementById('cars-fe-address-postcode-manual-error'); 3629 if (!err) { 3630 return; 3631 } 3632 const show = Boolean(invalid); 3633 err.hidden = !show; 3634 err.setAttribute('aria-hidden', show ? 'false' : 'true'); 3635 }; 3636 3637 const syncLiveManualUkPostcodeFeedback = () => { 3638 const pc = String(val('cars-fe-address-postcode-manual') || '').trim(); 3639 // A part-typed postcode is always flagged live; an empty one waits for touched/Next. 3640 const show = pc === '' 3641 ? feFieldHighlightAllowed('address', 'cars-fe-address-postcode-manual') 3642 : !ukPostcodeLooksComplete(pc); 3643 financeEligibilityMarkFieldInvalid('cars-fe-address-postcode-manual', show); 3644 syncManualUkPostcodeError(pc !== '' && !ukPostcodeLooksComplete(pc)); 3645 }; 3646 3647 /** UK national: accept 7xxxxxxxxx (no leading 0); payload uses 07⦠for +44 dial context. */ 3648 const normalizeUkNationalForPayload = (iso, dial, nationalRaw) => { 3649 const trimmed = String(nationalRaw || '').trim(); 3650 const compact = trimmed.replace(/[\s()-]/g, ''); 3651 if (/^\+44\d{10}$/.test(compact)) { 3652 return `0${compact.slice(3)}`; 3653 } 3654 const digits = trimmed.replace(/\D/g, ''); 3655 const dIso = String(iso || '').toLowerCase(); 3656 const dDial = String(dial || '').replace(/\D/g, ''); 3657 const uk = dIso === 'gb' || dDial === '44'; 3658 if (uk && /^7\d{9}$/.test(digits)) { 3659 return `0${digits}`; 3660 } 3661 return trimmed; 3662 }; 3663 3664 const personNameOk = (v) => { 3665 const s = String(v || '').trim(); 3666 return s.length > 0 && !/\d/.test(s); 3667 }; 3668 3669 const stripDigitsFromPersonNameInput = (input) => { 3670 if (!input || input.tagName !== 'INPUT') { 3671 return; 3672 } 3673 const prev = String(input.value || ''); 3674 const next = prev.replace(/\d+/g, ''); 3675 if (next === prev) { 3676 return; 3677 } 3678 const start = input.selectionStart; 3679 const end = input.selectionEnd; 3680 input.value = next; 3681 if (typeof start === 'number' && typeof end === 'number') { 3682 const removedBefore = (prev.slice(0, start).match(/\d/g) || []).length; 3683 const removedInSel = (prev.slice(start, end).match(/\d/g) || []).length; 3684 const nextStart = Math.max(0, start - removedBefore);
3685 const nextEnd = Math.max(nextStart, end - removedBefore - removedInSel); 3686 try { 3687 input.setSelectionRange(nextStart, nextEnd); 3688 } catch { 3689 /* ignore */ 3690 } 3691 } 3692 }; 3693 3694 const applyDetailsFieldHighlights = () => { 3695 const host = feFormDensityComposed && personalGroupedStep ? personalGroupedStep : detailsStep; 3696 if (!host) { 3697 return; 3698 } 3699 const mark = feStepFieldMarker('details'); 3700 mark('cars-fe-title', !val('cars-fe-title')); 3701 mark('cars-fe-first-name', !personNameOk(val('cars-fe-first-name'))); 3702 mark('cars-fe-last-name', !personNameOk(val('cars-fe-last-name'))); 3703 const dobRaw = val('cars-fe-dob'); 3704 mark('cars-fe-dob', dobJudgeable(dobRaw) && !dobIsoOk(dobRaw)); 3705 mark('cars-fe-marital', !val('cars-fe-marital')); 3706 syncDobAgeError(); 3707 }; 3708 3709 const isDetailsFieldsValid = () => 3710 Boolean( 3711 val('cars-fe-title') && 3712 personNameOk(val('cars-fe-first-name')) && 3713 personNameOk(val('cars-fe-last-name')) && 3714 dobIsoOk(val('cars-fe-dob')) && 3715 val('cars-fe-marital'), 3716 ); 3717 3718 const validateDetails = () => { 3719 syncDobPlaceholderState(); 3720 const ok = isDetailsFieldsValid(); 3721 applyDetailsFieldHighlights(); 3722 setBtnNextDisabled(nextBtn, !ok); 3723 return ok; 3724 }; 3725 3726 const syncDobPlaceholderState = () => { 3727 const dob = document.getElementById('cars-fe-dob'); 3728 const shell = dob?.closest('.cars-fe-field-group__shell--dob'); 3729 if (!shell) { 3730 return; 3731 } 3732 shell.classList.toggle('cars-fe-field-group__shell--dob-has-value', String(dob.value || '').trim() !== ''); 3733 }; 3734 3735 const applyContactFieldHighlights = () => { 3736 const host = feFormDensityComposed && personalGroupedStep ? personalGroupedStep : contactStep; 3737 if (!host) { 3738 return; 3739 } 3740 const emailRaw = val('cars-fe-email'); 3741 const emailWrongFormat = !emailCanStillBecomeValid(emailRaw); 3742 const emailInvalid = !emailOk(emailRaw); 3743 // Shown while the user is still in the field once the address can never be valid, or once it 3744 // is complete enough to judge â otherwise Next sits disabled with nothing saying why. 3745 const emailLive = emailWrongFormat || (emailInvalid && emailLooksComplete(emailRaw)); 3746 // Touched stays set for the rest of the journey, so without this an address the user came 3747 // back to would be flagged again from its first keystroke. A half-typed address is only 3748 // flagged once they leave the field, or Next blurs it for them. 3749 const emailGated = feFieldBeingEdited !== 'cars-fe-email' && feFieldHighlightAllowed('contact', 'cars-fe-email'); 3750 financeEligibilityMarkFieldInvalid('cars-fe-email', emailLive || (emailInvalid && emailGated)); 3751 syncEmailFormatError( 3752 emailLive || (emailInvalid && String(emailRaw || '').trim() !== '' && emailGated), 3753 ); 3754 syncLivePhoneFormatFeedback(); 3755 }; 3756 3757 const isContactFieldsValid = () => emailOk(val('cars-fe-email')) && nationalPhoneOk(val('cars-fe-phone-national')); 3758 3759 const validateContact = () => { 3760 const ok = isContactFieldsValid(); 3761 applyContactFieldHighlights(); 3762 setBtnNextDisabled(contactNextBtn, !ok); 3763 return ok; 3764 }; 3765 3766 const applyMoreFieldHighlights = () => { 3767 const host = feFormDensityComposed && personalGroupedStep ? personalGroupedStep : moreStep; 3768 if (!host) { 3769 return; 3770 } 3771 const mark = feStepFieldMarker('more'); 3772 mark('cars-fe-license-type', !val('cars-fe-license-type')); 3773 mark('cars-fe-dependants', !val('cars-fe-dependants')); 3774 }; 3775 3776 const isMoreFieldsValid = () => Boolean(val('cars-fe-license-type') && val('cars-fe-dependants')); 3777 3778 const validateMore = () => { 3779 const ok = isMoreFieldsValid(); 3780 applyMoreFieldHighlights(); 3781 setBtnNextDisabled(moreNextBtn, !ok); 3782 return ok; 3783 }; 3784
3785 const validatePersonalGrouped = () => { 3786 if (!personalGroupedNextBtn) { 3787 return false; 3788 } 3789 syncDobPlaceholderState(); 3790 const ok = isDetailsFieldsValid() && isContactFieldsValid() && isMoreFieldsValid(); 3791 applyDetailsFieldHighlights(); 3792 applyContactFieldHighlights(); 3793 applyMoreFieldHighlights(); 3794 setBtnNextDisabled(personalGroupedNextBtn, !ok); 3795 return ok; 3796 }; 3797 3798 const employmentTenureTotalMonthsFromStrings = (yStr, mStr) => { 3799 if (yStr === '' || mStr === '') { 3800 return null; 3801 } 3802 const y = parseInt(String(yStr), 10) || 0; 3803 const m = parseInt(String(mStr), 10) || 0; 3804 return y * 12 + m; 3805 }; 3806 3807 const employmentEntryTenureMonths = (entry) => { 3808 if (!entry) { 3809 return null; 3810 } 3811 return employmentTenureTotalMonthsFromStrings( 3812 entry.years != null ? String(entry.years) : '', 3813 entry.months != null ? String(entry.months) : '', 3814 ); 3815 }; 3816 3817 const getTotalEmploymentMonthsFromSavedEntries = () => 3818 savedEmploymentEntries.reduce((sum, e) => sum + (employmentEntryTenureMonths(e) || 0), 0); 3819 3820 /** Months counted toward the 3-year rule: saved rows, with the row under edit replaced by live form values. */ 3821 const getEmploymentHistoryMonthsForValidation = () => { 3822 if (editingEmploymentIndex != null && typeof editingEmploymentIndex === 'number') { 3823 const sumOthers = savedEmploymentEntries.reduce((sum, e, i) => { 3824 if (i === editingEmploymentIndex) { 3825 return sum; 3826 } 3827 return sum + (employmentEntryTenureMonths(e) || 0); 3828 }, 0); 3829 const live = employmentTenureTotalMonthsFromStrings( 3830 val('cars-fe-employment-years'), 3831 val('cars-fe-employment-months'), 3832 ); 3833 return sumOthers + (live != null ? live : 0); 3834 } 3835 return getTotalEmploymentMonthsFromSavedEntries(); 3836 }; 3837 3838 const syncEmploymentHistoryAlert = () => { 3839 const alertEl = document.getElementById('cars-fe-employment-history-alert'); 3840 if (!alertEl) { 3841 return; 3842 } 3843 const host = getEmploymentUiHost(); 3844 if (!host || host.hidden) { 3845 alertEl.hidden = true; 3846 return; 3847 } 3848 const savedPanel = document.getElementById('cars-fe-employment-saved-panel'); 3849 const panelVisible = savedPanel && savedPanel.hidden === false; 3850 const total = getTotalEmploymentMonthsFromSavedEntries(); 3851 const show = panelVisible && savedEmploymentEntries.length > 0 && total < 36; 3852 alertEl.hidden = !show; 3853 }; 3854 3855 const isPastEmploymentFormContext = () => { 3856 if (editingEmploymentIndex != null && editingEmploymentIndex > 0) { 3857 return true; 3858 } 3859 if (editingEmploymentIndex != null && editingEmploymentIndex === 0) { 3860 return false; 3861 } 3862 return savedEmploymentEntries.length >= 1; 3863 }; 3864 3865 const EMPLOYMENT_STATUS_AUTOFILL_ALL_FIELDS = new Set([ 3866 'Retired', 3867 'Student', 3868 'Homemaker', 3869 'Unemployed', 3870 'On Benefits', 3871 ]); 3872 3873 const getEmploymentDurationQuestionLabel = (status) => { 3874 const s = String(status || '').trim(); 3875 switch (s) { 3876 case 'Retired': 3877 return 'How long have you been retired?'; 3878 case 'Student': 3879 return 'How long have you been a student?'; 3880 case 'Homemaker': 3881 return 'How long have you been a homemaker?'; 3882 case 'Unemployed': 3883 return 'How long have you been unemployed?'; 3884 case 'On Benefits': 3885 return 'How long have you been on benefits?'; 3886 default: 3887 return 'Employment duration'; 3888 } 3889 }; 3890 3891 const syncEmploymentDurationLabelForStatus = (status) => { 3892 const el = document.getElementById('cars-fe-employment-duration-label'); 3893 if (!el) { 3894 return; 3895 }
3896 el.textContent = getEmploymentDurationQuestionLabel(status); 3897 }; 3898 3899 const clearEmploymentAutofillFields = () => { 3900 const employer = document.getElementById('cars-fe-employment-employer-name'); 3901 const occHidden = document.getElementById('cars-fe-employment-occupation'); 3902 const occQ = document.getElementById('cars-fe-employment-occupation-q'); 3903 const basis = document.getElementById('cars-fe-employment-occupational-basis'); 3904 const occType = document.getElementById('cars-fe-employment-occupational-type'); 3905 const years = document.getElementById('cars-fe-employment-years'); 3906 const months = document.getElementById('cars-fe-employment-months'); 3907 if (employer) { 3908 employer.value = ''; 3909 } 3910 if (occHidden) { 3911 occHidden.value = ''; 3912 } 3913 if (occQ) { 3914 occQ.value = ''; 3915 } 3916 if (basis) { 3917 basis.value = ''; 3918 } 3919 if (occType) { 3920 occType.value = ''; 3921 } 3922 if (years) { 3923 years.value = ''; 3924 } 3925 if (months) { 3926 months.value = financeEligibilityFieldDefaultValue('cars-fe-employment-months'); 3927 } 3928 financeEligibilityCloseOccupationPanels(getEmploymentUiHost()); 3929 syncAllFinanceDurationSelectShells(); 3930 }; 3931 3932 /** 3933 * Autofill statuses: set employer/occupation/basis/type. Any other status: clear those fields and years 3934 * (leaving autofill); months returns to its preselected 0 rather than blank. 3935 */ 3936 const applyEmploymentStatusAutofill = (status) => { 3937 const employer = document.getElementById('cars-fe-employment-employer-name'); 3938 const occHidden = document.getElementById('cars-fe-employment-occupation'); 3939 const occQ = document.getElementById('cars-fe-employment-occupation-q'); 3940 const basis = document.getElementById('cars-fe-employment-occupational-basis'); 3941 const occType = document.getElementById('cars-fe-employment-occupational-type'); 3942 3943 if (!EMPLOYMENT_STATUS_AUTOFILL_ALL_FIELDS.has(status)) { 3944 clearEmploymentAutofillFields(); 3945 return; 3946 } 3947 3948 if (!employer || !occHidden || !basis || !occType) { 3949 return; 3950 } 3951 const label = status; 3952 employer.value = label; 3953 occHidden.value = label; 3954 if (occQ) { 3955 occQ.value = label; 3956 } 3957 basis.value = label; 3958 occType.value = label; 3959 syncAllFinanceDurationSelectShells(); 3960 }; 3961 3962 const syncEmploymentFormFieldLabelsForContext = () => { 3963 const past = isPastEmploymentFormContext(); 3964 const empNameInput = document.getElementById('cars-fe-employment-employer-name'); 3965 const occHidden = document.getElementById('cars-fe-employment-occupation'); 3966 const empWrap = empNameInput ? empNameInput.closest('.cars-fe-field-group') : null; 3967 const occWrap = occHidden ? occHidden.closest('.cars-fe-field-group') : null; 3968 const empLab = empWrap ? empWrap.querySelector('.cars-fe-field-group__label') : null; 3969 const occLab = occWrap ? occWrap.querySelector('.cars-fe-field-group__label') : null; 3970 if (empLab) { 3971 empLab.textContent = past ? 'Past employer' : 'Employers name'; 3972 } 3973 if (occLab) { 3974 occLab.textContent = past ? 'Past occupation' : 'Occupation'; 3975 } 3976 }; 3977 3978 const employmentFormValidForSave = () => { 3979 const hasStatus = Boolean(val('cars-fe-employment-status')); 3980 const hasDetails = 3981 Boolean(val('cars-fe-employment-employer-name')) && 3982 Boolean(val('cars-fe-employment-occupation')) && 3983 Boolean(val('cars-fe-employment-occupational-basis')) && 3984 Boolean(val('cars-fe-employment-occupational-type')) && 3985 durationYearsMonthsOk('cars-fe-employment-years', 'cars-fe-employment-months'); 3986 return hasStatus && hasDetails; 3987 }; 3988 3989 const applyEmploymentFormFieldHighlights = () => { 3990 const host = getEmploymentUiHost(); 3991 if (!host) { 3992 return; 3993 } 3994 const entryBlock = document.getElementById('cars-fe-employment-entry-block'); 3995 if (!entryBlock || entryBlock.hidden) { 3996 return; 3997 } 3998 const durationInvalid = !durationYearsMonthsOk( 3999 'cars-fe-employment-years', 4000 'cars-fe-employment-months', 4001 ); 4002 const mark = feStepFieldMarker('employment'); 4003 mark('cars-fe-employment-status', !val('cars-fe-employment-status')); 4004 mark('cars-fe-employment-employer-name', !val('cars-fe-employment-employer-name')); 4005 mark('cars-fe-employment-occupation', !val('cars-fe-employment-occupation')); 4006 mark('cars-fe-employment-occupational-basis', !val('cars-fe-employment-occupational-basis')); 4007 mark('cars-fe-employment-occupational-type', !val('cars-fe-employment-occupational-type')); 4008 mark('cars-fe-employment-years', durationInvalid); 4009 mark('cars-fe-employment-months', durationInvalid); 4010 }; 4011 4012 const isEmploymentReadyForNext = () => { 4013 syncEmploymentSummaryUiState();
4014 const totalMonths = getEmploymentHistoryMonthsForValidation(); 4015 const employmentHistoryInsufficient = 4016 savedEmploymentEntries.length > 0 && totalMonths < 36; 4017 const midEmploymentEdit = editingEmploymentId != null && editingEmploymentIndex != null; 4018 return savedEmploymentEntries.length > 0 && !employmentHistoryInsufficient && !midEmploymentEdit; 4019 }; 4020 4021 const validateEmployment = () => { 4022 const groupedActive = 4023 feFormDensityComposed && incomeOutgoingsGroupedStep && !incomeOutgoingsGroupedStep.hidden; 4024 const controlActive = employmentStep && !employmentStep.hidden; 4025 if (!groupedActive && !controlActive) { 4026 if (employmentNextBtn) { 4027 setBtnNextDisabled(employmentNextBtn, true); 4028 } 4029 return false; 4030 } 4031 const ok = isEmploymentReadyForNext(); 4032 applyEmploymentFormFieldHighlights(); 4033 if (controlActive && employmentNextBtn) { 4034 setBtnNextDisabled(employmentNextBtn, !ok); 4035 } 4036 syncEmploymentHistoryAlert(); 4037 return ok; 4038 }; 4039 4040 financeEligibilityInitOccupationComboboxes(() => { 4041 syncEmploymentFormFieldLabelsForContext(); 4042 syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status')); 4043 if (feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped') { 4044 validateIncomeOutgoingsGrouped(); 4045 } else { 4046 validateEmployment(); 4047 } 4048 }); 4049 4050 financeEligibilityInitDurationListboxes(() => { 4051 const addressStepOpen = addressStep && addressStep.hidden === false; 4052 const employmentHost = getEmploymentUiHost(); 4053 const employmentStepOpen = employmentHost && employmentHost.hidden === false; 4054 if (addressStepOpen) { 4055 syncAddressMonthsDisabledForFifteenPlus(); 4056 validateAddress(); 4057 } 4058 if (employmentStepOpen) { 4059 if (feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped') { 4060 validateIncomeOutgoingsGrouped(); 4061 } else { 4062 validateEmployment(); 4063 } 4064 } 4065 }); 4066 4067 const parseSterlingInputToNumber = (raw) => { 4068 const s = String(raw || '') 4069 .replace(/£/g, '') 4070 .replace(/,/g, '') 4071 .replace(/\s+/g, '') 4072 .trim(); 4073 if (s === '') { 4074 return NaN; 4075 } 4076 const n = parseFloat(s); 4077 return n; 4078 }; 4079 4080 const stripSterlingCommas = (raw) => 4081 String(raw || '') 4082 .replace(/£/g, '') 4083 .replace(/,/g, '') 4084 .replace(/\s+/g, '') 4085 .trim(); 4086 4087 /** Digits only â en-GB thousands separators for display (e.g. 25000 â 25,000). */ 4088 const formatSterlingThousandsDisplay = (raw) => { 4089 const digits = String(raw || '').replace(/[^\d]/g, ''); 4090 if (digits === '') { 4091 return ''; 4092 } 4093 const normalized = digits.replace(/^0+(?=\d)/, ''); 4094 return Number(normalized).toLocaleString('en-GB'); 4095 }; 4096 4097 /** Review summary money: £25,000 (empty â blank so callers can fall back). */ 4098 const formatSterlingForReview = (raw) => { 4099 const formatted = formatSterlingThousandsDisplay(raw); 4100 return formatted ? `£${formatted}` : ''; 4101 }; 4102 4103 const applySterlingThousandsFormatting = (inputEl) => { 4104 if (!inputEl || inputEl.tagName !== 'INPUT') { 4105 return; 4106 } 4107 const prev = String(inputEl.value || ''); 4108 const digitsBeforeCaret = prev.slice(0, inputEl.selectionStart ?? prev.length).replace(/[^\d]/g, '').length; 4109 const next = formatSterlingThousandsDisplay(prev); 4110 if (next === prev) { 4111 return; 4112 } 4113 inputEl.value = next; 4114 let caret = 0; 4115 let seen = 0; 4116 while (caret < next.length && seen < digitsBeforeCaret) { 4117 if (/\d/.test(next[caret])) { 4118 seen += 1; 4119 } 4120 caret += 1; 4121 } 4122 try { 4123 inputEl.setSelectionRange(caret, caret); 4124 } catch { 4125 /* ignore */ 4126 } 4127 }; 4128 4129 const applyIncomeOutgoingsFieldHighlights = () => { 4130 const host = 4131 feFormDensityComposed && incomeOutgoingsGroupedStep ? incomeOutgoingsGroupedStep : incomeOutgoingsStep; 4132 if (!host) { 4133 return; 4134 } 4135 const annualBeforeTax = parseSterlingInputToNumber(val('cars-fe-income-annual-before-tax')); 4136 const monthlyTakeHome = parseSterlingInputToNumber(val('cars-fe-income-monthly-take-home')); 4137 const otherAnnual = parseSterlingInputToNumber(val('cars-fe-income-annual-household-other')); 4138 const mark = feStepFieldMarker('income'); 4139 mark('cars-fe-income-annual-before-tax', Number.isNaN(annualBeforeTax) || annualBeforeTax <= 0); 4140 mark('cars-fe-income-monthly-take-home', Number.isNaN(monthlyTakeHome) || monthlyTakeHome <= 0); 4141 mark('cars-fe-income-annual-household-other', Number.isNaN(otherAnnual) || otherAnnual < 0); 4142 }; 4143 4144 const isIncomeOutgoingsFieldsValid = () => { 4145 const annualBeforeTax = parseSterlingInputToNumber(val('cars-fe-income-annual-before-tax')); 4146 const monthlyTakeHome = parseSterlingInputToNumber(val('cars-fe-income-monthly-take-home')); 4147 const otherAnnual = parseSterlingInputToNumber(val('cars-fe-income-annual-household-other')); 4148 return ( 4149 !Number.isNaN(annualBeforeTax) && 4150 annualBeforeTax > 0 && 4151 !Number.isNaN(monthlyTakeHome) && 4152 monthlyTakeHome > 0 && 4153 !Number.isNaN(otherAnnual) && 4154 otherAnnual >= 0 4155 ); 4156 }; 4157 4158 const validateIncomeOutgoings = () => { 4159 if (!incomeNextBtn) { 4160 return false;
4161 } 4162 const controlActive = incomeOutgoingsStep && !incomeOutgoingsStep.hidden; 4163 if (!controlActive) { 4164 setBtnNextDisabled(incomeNextBtn, true); 4165 return false; 4166 } 4167 const ok = isIncomeOutgoingsFieldsValid(); 4168 applyIncomeOutgoingsFieldHighlights(); 4169 setBtnNextDisabled(incomeNextBtn, !ok); 4170 return ok; 4171 }; 4172 4173 const syncOutgoingsConditionalVisibility = () => { 4174 const repayFollow = document.getElementById('cars-fe-outgoings-repay-followup'); 4175 const downturnFollow = document.getElementById('cars-fe-outgoings-downturn-followup'); 4176 const repay = val('cars-fe-outgoings-repay-existing-finance'); 4177 const downturn = val('cars-fe-outgoings-downturn-expected'); 4178 if (repayFollow) { 4179 repayFollow.hidden = repay !== 'Yes'; 4180 } 4181 if (downturnFollow) { 4182 downturnFollow.hidden = downturn !== 'Yes'; 4183 } 4184 }; 4185 4186 const clearOutgoingsRepayFollowupFields = () => { 4187 const el = document.getElementById('cars-fe-outgoings-existing-loan-monthly-payment'); 4188 if (el) { 4189 el.value = ''; 4190 } 4191 }; 4192 4193 const clearOutgoingsDownturnFollowupFields = () => { 4194 const reason = document.getElementById('cars-fe-outgoings-downturn-reason'); 4195 if (reason) { 4196 reason.value = ''; 4197 } 4198 const reduced = document.getElementById('cars-fe-outgoings-reduced-annual-income-before-tax'); 4199 if (reduced) { 4200 reduced.value = ''; 4201 } 4202 }; 4203 4204 const applyOutgoingsFinalFieldHighlights = () => { 4205 const host = 4206 feFormDensityComposed && incomeOutgoingsGroupedStep ? incomeOutgoingsGroupedStep : outgoingsFinalStep; 4207 if (!host) { 4208 return; 4209 } 4210 const mortgageShare = parseSterlingInputToNumber(val('cars-fe-outgoings-monthly-mortgage-rent-share')); 4211 const otherBills = parseSterlingInputToNumber(val('cars-fe-outgoings-other-monthly-bills')); 4212 const repay = val('cars-fe-outgoings-repay-existing-finance'); 4213 const downturn = val('cars-fe-outgoings-downturn-expected'); 4214 const yesNoOk = (v) => v === 'Yes' || v === 'No'; 4215 const mark = feStepFieldMarker('outgoings'); 4216 4217 mark('cars-fe-outgoings-monthly-mortgage-rent-share', Number.isNaN(mortgageShare) || mortgageShare < 0); 4218 mark('cars-fe-outgoings-other-monthly-bills', Number.isNaN(otherBills) || otherBills < 0); 4219 mark('cars-fe-outgoings-repay-existing-finance', !yesNoOk(repay)); 4220 mark('cars-fe-outgoings-downturn-expected', !yesNoOk(downturn)); 4221 4222 if (repay === 'Yes') { 4223 const existingLoanMonthly = parseSterlingInputToNumber(val('cars-fe-outgoings-existing-loan-monthly-payment')); 4224 mark( 4225 'cars-fe-outgoings-existing-loan-monthly-payment', 4226 Number.isNaN(existingLoanMonthly) || existingLoanMonthly < 0, 4227 ); 4228 } else { 4229 financeEligibilityMarkFieldInvalid('cars-fe-outgoings-existing-loan-monthly-payment', false); 4230 } 4231 4232 if (downturn === 'Yes') { 4233 const reducedAnnual = parseSterlingInputToNumber(val('cars-fe-outgoings-reduced-annual-income-before-tax')); 4234 mark('cars-fe-outgoings-downturn-reason', !val('cars-fe-outgoings-downturn-reason')); 4235 mark('cars-fe-outgoings-reduced-annual-income-before-tax', Number.isNaN(reducedAnnual) || reducedAnnual < 0); 4236 } else { 4237 financeEligibilityMarkFieldInvalid('cars-fe-outgoings-downturn-reason', false); 4238 financeEligibilityMarkFieldInvalid('cars-fe-outgoings-reduced-annual-income-before-tax', false); 4239 } 4240 }; 4241 4242 const isOutgoingsFinalFieldsValid = () => { 4243 syncOutgoingsConditionalVisibility(); 4244 4245 const mortgageShare = parseSterlingInputToNumber(val('cars-fe-outgoings-monthly-mortgage-rent-share')); 4246 const otherBills = parseSterlingInputToNumber(val('cars-fe-outgoings-other-monthly-bills')); 4247 const repay = val('cars-fe-outgoings-repay-existing-finance'); 4248 const downturn = val('cars-fe-outgoings-downturn-expected'); 4249 const yesNoOk = (v) => v === 'Yes' || v === 'No'; 4250 let ok = 4251 !Number.isNaN(mortgageShare) && 4252 mortgageShare >= 0 && 4253 !Number.isNaN(otherBills) && 4254 otherBills >= 0 && 4255 yesNoOk(repay) && 4256 yesNoOk(downturn); 4257 4258 if (ok && repay === 'Yes') { 4259 const existingLoanMonthly = parseSterlingInputToNumber(val('cars-fe-outgoings-existing-loan-monthly-payment')); 4260 ok = ok && !Number.isNaN(existingLoanMonthly) && existingLoanMonthly >= 0; 4261 } 4262
4263 if (ok && downturn === 'Yes') { 4264 const downturnReason = val('cars-fe-outgoings-downturn-reason'); 4265 const reducedAnnual = parseSterlingInputToNumber(val('cars-fe-outgoings-reduced-annual-income-before-tax')); 4266 ok = ok && Boolean(downturnReason) && !Number.isNaN(reducedAnnual) && reducedAnnual >= 0; 4267 } 4268 return ok; 4269 }; 4270 4271 const validateOutgoingsFinal = () => { 4272 if (!outgoingsSummaryBtn) { 4273 return false; 4274 } 4275 if (!outgoingsFinalStep || outgoingsFinalStep.hidden) { 4276 setBtnNextDisabled(outgoingsSummaryBtn, true); 4277 return false; 4278 } 4279 const ok = isOutgoingsFinalFieldsValid(); 4280 applyOutgoingsFinalFieldHighlights(); 4281 setBtnNextDisabled(outgoingsSummaryBtn, !ok); 4282 return ok; 4283 }; 4284 4285 const validateIncomeOutgoingsGrouped = () => { 4286 if (!incomeOutgoingsGroupedSummaryBtn) { 4287 return false; 4288 } 4289 if (!incomeOutgoingsGroupedStep || incomeOutgoingsGroupedStep.hidden) { 4290 setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, true); 4291 return false; 4292 } 4293 const employmentOk = validateEmployment(); 4294 const incomeOk = isIncomeOutgoingsFieldsValid(); 4295 applyIncomeOutgoingsFieldHighlights(); 4296 const outgoingsOk = isOutgoingsFinalFieldsValid(); 4297 applyOutgoingsFinalFieldHighlights(); 4298 const ok = employmentOk && incomeOk && outgoingsOk; 4299 setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, !ok); 4300 return ok; 4301 }; 4302 4303 /** Strict UK postcode format â use only for the manual postcode field, not Postcoder search. */ 4304 const ukPostcodeLooksComplete = (raw) => { 4305 const x = String(raw || '') 4306 .trim() 4307 .toUpperCase() 4308 .replace(/\s+/g, ' '); 4309 if (!x) { 4310 return false; 4311 } 4312 // Full UK postcode (outward + inward), optional single space. Rejects non-UK formats. 4313 return /^(GIR 0AA|[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2})$/.test(x) || 4314 /^(GIR0AA|[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2})$/.test(x.replace(/\s/g, '')); 4315 }; 4316 4317 const parseDurationYearsMonths = (yearsId, monthsId) => { 4318 const yRaw = val(yearsId); 4319 const mRaw = val(monthsId); 4320 if (yRaw === '' || mRaw === '') { 4321 return null; 4322 } 4323 const years = Number(yRaw); 4324 const months = Number(mRaw); 4325 if (!Number.isFinite(years) || !Number.isFinite(months) || years < 0 || months < 0) { 4326 return null; 4327 } 4328 return { years, months }; 4329 }; 4330 4331 /** At least one month of history; 15+ years (address) counts as valid without months. */ 4332 const durationYearsMonthsOk = (yearsId, monthsId, opts = {}) => { 4333 const parsed = parseDurationYearsMonths(yearsId, monthsId); 4334 if (!parsed) { 4335 return false; 4336 } 4337 const plusYears = opts.plusYearsValue; 4338 if (plusYears != null && parsed.years >= plusYears) { 4339 return true; 4340 } 4341 return parsed.years * 12 + parsed.months > 0; 4342 }; 4343 4344 const syncAddressMonthsDisabledForFifteenPlus = () => { 4345 const yearsEl = document.getElementById('cars-fe-address-years-at'); 4346 const monthsEl = document.getElementById('cars-fe-address-months-at'); 4347 const monthsShell = monthsEl?.closest?.('.cars-fe-field-group__shell--duration'); 4348 const monthsTrigger = monthsShell?.querySelector?.('.cars-fe-duration-listbox__trigger'); 4349 if (!yearsEl || !monthsEl || !monthsShell || !monthsTrigger) { 4350 return; 4351 } 4352 const isFifteenPlus = String(yearsEl.value || '').trim() === '15'; 4353 monthsShell.classList.toggle('cars-fe-field-group__shell--duration-disabled', isFifteenPlus); 4354 monthsTrigger.disabled = isFifteenPlus; 4355 monthsTrigger.setAttribute('aria-disabled', isFifteenPlus ? 'true' : 'false'); 4356 if (isFifteenPlus) { 4357 const monthsPanel = monthsShell.querySelector('.cars-fe-duration-listbox__panel'); 4358 if (monthsPanel && !monthsPanel.hidden) { 4359 financeEligibilityCloseDurationListboxes(addressStep || document.getElementById('cars-fe-step-address')); 4360 } 4361 if (String(monthsEl.value || '') !== '0') { 4362 monthsEl.value = '0'; 4363 financeEligibilitySyncDurationListboxShell(monthsShell); 4364 } 4365 } 4366 }; 4367 4368 const syncAddressSelectBlockVisibility = () => { 4369 const block = document.getElementById('cars-fe-address-select-block'); 4370 if (!block || !addressStep) { 4371 return; 4372 } 4373 const manual = addressStep.classList.contains('cars-fe-address--manual'); 4374 const show = !manual && ukPostcodeLooksComplete(val('cars-fe-address-postcode')); 4375 block.hidden = !show; 4376 }; 4377 4378 const newAddressEntryId = () => `cars-fe-addr-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; 4379 4380 const newEmploymentEntryId = () => `cars-fe-emp-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; 4381 4382 const clearEmploymentDetailFormFields = () => { 4383 [ 4384 'cars-fe-employment-employer-name', 4385 'cars-fe-employment-occupation', 4386 'cars-fe-employment-occupation-q', 4387 'cars-fe-employment-occupational-basis', 4388 'cars-fe-employment-occupational-type', 4389 'cars-fe-employment-years', 4390 'cars-fe-employment-months', 4391 ].forEach((id) => { 4392 const el = document.getElementById(id); 4393 if (el) {
4394 el.value = financeEligibilityFieldDefaultValue(id); 4395 } 4396 }); 4397 financeEligibilityCloseOccupationPanels(getEmploymentUiHost()); 4398 syncAllFinanceDurationSelectShells(); 4399 syncEmploymentFormFieldLabelsForContext(); 4400 syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status')); 4401 }; 4402 4403 const buildEmploymentEntryFromForm = (idOverride) => ({ 4404 id: typeof idOverride === 'string' && idOverride.length > 0 ? idOverride : newEmploymentEntryId(), 4405 status: val('cars-fe-employment-status'), 4406 employerName: val('cars-fe-employment-employer-name'), 4407 occupation: val('cars-fe-employment-occupation'), 4408 occupationalBasis: val('cars-fe-employment-occupational-basis'), 4409 occupationalType: val('cars-fe-employment-occupational-type'), 4410 phoneDial: '', 4411 phoneCountry: '', 4412 phoneNational: '', 4413 years: val('cars-fe-employment-years'), 4414 months: val('cars-fe-employment-months'), 4415 pastEmployerName: '', 4416 pastOccupation: '', 4417 }); 4418 4419 const applyEmploymentEntryToForm = (entry) => { 4420 const setVal = (id, v) => { 4421 const el = document.getElementById(id); 4422 if (el) { 4423 el.value = v != null ? String(v) : ''; 4424 } 4425 }; 4426 setVal('cars-fe-employment-employer-name', entry.employerName); 4427 setVal('cars-fe-employment-occupation', entry.occupation); 4428 setVal('cars-fe-employment-occupation-q', entry.occupation); 4429 setVal('cars-fe-employment-occupational-basis', entry.occupationalBasis); 4430 setVal('cars-fe-employment-occupational-type', entry.occupationalType); 4431 setVal('cars-fe-employment-years', entry.years); 4432 setVal('cars-fe-employment-months', entry.months); 4433 const hiddenStatus = document.getElementById('cars-fe-employment-status'); 4434 if (hiddenStatus) { 4435 hiddenStatus.value = entry.status || ''; 4436 } 4437 const pillGroup = getEmploymentUiHost()?.querySelector('.cars-fe-pills--employment'); 4438 if (pillGroup) { 4439 pillGroup.querySelectorAll('.cars-fe-pill').forEach((p) => { 4440 p.classList.toggle('is-selected', (p.dataset.value || '') === (entry.status || '')); 4441 }); 4442 } 4443 syncAllFinanceDurationSelectShells(); 4444 syncEmploymentFormFieldLabelsForContext(); 4445 }; 4446 4447 const formatEmploymentDurationSummary = (yearsStr, monthsStr) => { 4448 const y = parseInt(String(yearsStr || ''), 10) || 0; 4449 const m = parseInt(String(monthsStr || ''), 10) || 0; 4450 const yPart = y === 1 ? '1 year' : `${y} years`; 4451 const mPart = m === 1 ? '1 month' : `${m} months`; 4452 if (y > 0 && m > 0) { 4453 return `${yPart}, ${mPart}`; 4454 } 4455 if (y > 0) { 4456 return yPart; 4457 } 4458 return mPart; 4459 }; 4460 4461 const createEmploymentSavedFieldBlock = (labelText, valueText) => { 4462 const block = document.createElement('div'); 4463 block.className = 'cars-fe-employment-saved-field'; 4464 const labelRow = document.createElement('div'); 4465 labelRow.className = 'cars-fe-employment-saved-field__label-row'; 4466 const lab = document.createElement('p'); 4467 lab.className = 'cars-fe-employment-saved-field__label'; 4468 lab.textContent = labelText; 4469 labelRow.appendChild(lab); 4470 block.appendChild(labelRow); 4471 const valueEl = document.createElement('p'); 4472 valueEl.className = 'cars-fe-employment-saved-field__value'; 4473 valueEl.textContent = valueText || ''; 4474 block.appendChild(valueEl); 4475 return block; 4476 }; 4477 4478 const renderEmploymentSavedCards = () => { 4479 const mount = document.getElementById('cars-fe-employment-saved-mount'); 4480 if (!mount) { 4481 return; 4482 } 4483 mount.textContent = ''; 4484 savedEmploymentEntries.forEach((entry, index) => { 4485 const card = document.createElement('article'); 4486 card.className = 'cars-fe-employment-saved-card'; 4487 4488 const header = document.createElement('div'); 4489 header.className = 'cars-fe-employment-saved-card__header'; 4490 const title = document.createElement('p'); 4491 title.className = 'cars-fe-employment-saved-card__title'; 4492 title.textContent = index === 0 ? 'Current employment' : 'Previous employment'; 4493 const editBtn = document.createElement('button'); 4494 editBtn.type = 'button'; 4495 editBtn.className = 'cars-fe-employment-saved-edit-button'; 4496 editBtn.setAttribute('aria-label', 'Edit employment'); 4497 editBtn.setAttribute('data-cars-fe-employment-edit', entry.id); 4498 const editIcon = document.createElement('span'); 4499 editIcon.className = 'cars-fe-employment-saved-edit-button__icon'; 4500 editBtn.appendChild(editIcon); 4501 header.appendChild(title); 4502 header.appendChild(editBtn); 4503 card.appendChild(header); 4504 4505 const rule = document.createElement('div'); 4506 rule.className = 'cars-fe-employment-saved-card__rule'; 4507 card.appendChild(rule); 4508 4509 const columns = document.createElement('div'); 4510 columns.className = 'cars-fe-employment-saved-card__columns'; 4511 4512 const colLeft = document.createElement('div'); 4513 colLeft.className = 'cars-fe-employment-saved-card__col'; 4514 colLeft.appendChild(createEmploymentSavedFieldBlock('Employers n
4514ame', entry.employerName)); 4515 colLeft.appendChild(createEmploymentSavedFieldBlock('Occupation', entry.occupation)); 4516 colLeft.appendChild(createEmploymentSavedFieldBlock('Occupational basis', entry.occupationalBasis)); 4517 4518 const colRight = document.createElement('div'); 4519 colRight.className = 'cars-fe-employment-saved-card__col'; 4520 colRight.appendChild(createEmploymentSavedFieldBlock('Occupational type', entry.occupationalType)); 4521 colRight.appendChild( 4522 createEmploymentSavedFieldBlock( 4523 getEmploymentDurationQuestionLabel(entry.status), 4524 formatEmploymentDurationSummary(entry.years, entry.months), 4525 ), 4526 ); 4527 4528 columns.appendChild(colLeft); 4529 columns.appendChild(colRight); 4530 card.appendChild(columns); 4531 mount.appendChild(card); 4532 }); 4533 }; 4534 4535 const syncEmploymentSummaryUiState = () => { 4536 const host = getEmploymentUiHost(); 4537 if (!host) { 4538 return; 4539 } 4540 const savedPanel = document.getElementById('cars-fe-employment-saved-panel'); 4541 const addAnotherCard = document.getElementById('cars-fe-employment-add-another-card'); 4542 const entryBlock = document.getElementById('cars-fe-employment-entry-block'); 4543 const hasSaved = savedEmploymentEntries.length > 0; 4544 4545 renderEmploymentSavedCards(); 4546 4547 const entryFlowVisible = savedEmploymentEntries.length === 0 || employmentFormMode === 'editing'; 4548 4549 if (savedPanel) { 4550 savedPanel.hidden = !hasSaved; 4551 } 4552 if (addAnotherCard) { 4553 const historyIncomplete = getTotalEmploymentMonthsFromSavedEntries() < 36; 4554 const hideWhileAddingAnother = hasSaved && entryFlowVisible; 4555 addAnotherCard.hidden = !hasSaved || !historyIncomplete || hideWhileAddingAnother; 4556 } 4557 if (entryBlock) { 4558 entryBlock.hidden = !entryFlowVisible; 4559 } 4560 host.classList.toggle('cars-fe-employment--has-saved-summary', hasSaved); 4561 host.classList.toggle('cars-fe-employment--entry-flow-visible', entryFlowVisible); 4562 syncEmploymentFormFieldLabelsForContext(); 4563 syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status')); 4564 syncEmploymentHistoryAlert(); 4565 }; 4566 4567 const getTotalAddressHistoryMonthsFromEntries = () => 4568 savedAddressEntries.reduce((sum, e) => sum + (e.monthsAt || 0), 0); 4569 4570 const syncAddressHistoryAlert = () => { 4571 const alertEl = document.getElementById('cars-fe-address-history-alert'); 4572 if (!alertEl) { 4573 return; 4574 } 4575 if (!addressStep || addressStep.hidden) { 4576 alertEl.hidden = true; 4577 return; 4578 } 4579 const savedSummaryPanel = document.getElementById('cars-fe-address-saved-summary-panel'); 4580 const panelVisible = savedSummaryPanel && savedSummaryPanel.hidden === false; 4581 // Every saved address counts, whichever route added it, so the 3-year rule cannot be 4582 // sidestepped by an entry that carries no duration. 4583 const show = 4584 panelVisible && 4585 savedAddressEntries.length > 0 && 4586 getTotalAddressHistoryMonthsFromEntries() < 36; 4587 alertEl.hidden = !show; 4588 }; 4589 4590 const formatYearsMonthsAtSummary = (yearsStr, monthsStr) => { 4591 const y = parseInt(String(yearsStr || ''), 10); 4592 const m = parseInt(String(monthsStr || ''), 10); 4593 if (Number.isNaN(y) || Number.isNaN(m)) { 4594 return ''; 4595 } 4596 if (y === 15) { 4597 return '15+ years'; 4598 } 4599 const yearLabel = String(y); 4600 if (m === 0) { 4601 return `${yearLabel} ${y === 1 ? 'year' : 'years'}`; 4602 } 4603 const yPart = `${yearLabel} ${y === 1 ? 'year' : 'years'}`; 4604 const mPart = `${m} ${m === 1 ? 'month' : 'months'}`; 4605 return `${yPart}, ${mPart}`; 4606 }; 4607 4608 const parseLookupSelectedLineToEntryFields = (full, lookupPc) => { 4609 const parts = String(full || '') 4610 .split(',') 4611 .map((s) => s.trim()) 4612 .filter(Boolean); 4613 let line1 = ''; 4614 let townCounty = ''; 4615 let postcode = lookupPc; 4616 if (parts.length === 0) { 4617 return { line1: '', line2: '', townCounty: '', postcode }; 4618 } 4619 const last = parts[parts.length - 1]; 4620 const lastCompact = last.replace(/\s+/g, '').toUpperCase(); 4621 const lastLooksPc = 4622 lastCompact.length >= 5 && 4623 lastCompact.length <= 8 && 4624 /^[A-Z0-9]+$/.test(lastCompact); 4625 if (lastLooksPc && parts.length >= 2) { 4626 line1 = parts[0]; 4627 townCounty = parts.slice(1, -1).join(', ').trim(); 4628 postcode = last; 4629 } else { 4630 line1 = full; 4631 townCounty = ''; 4632 postcode = lookupPc; 4633 } 4634 return { line1, line2: '', townCounty, postcode }; 4635 }; 4636 4637 /**
4638 * Legacy select fallback, used only when Postcoder is unavailable. Picking an address there 4639 * used to save a lookup entry with no residency or time at address, so the 3-year history rule 4640 * could never be satisfied and Next stayed disabled with nothing flagged. Mirror what Postcoder 4641 * does on selection instead: prefill the manual card from the chosen line and ask for the 4642 * duration. 4643 */ 4644 const openManualCardForLookupSelection = (selectedLine) => { 4645 if (!addressStep || !String(selectedLine || '').trim()) { 4646 return; 4647 } 4648 const parsed = parseLookupSelectedLineToEntryFields(selectedLine, val('cars-fe-address-postcode')); 4649 const setFieldValue = (id, v) => { 4650 const el = document.getElementById(id); 4651 if (el) { 4652 el.value = v != null ? String(v) : ''; 4653 } 4654 }; 4655 setFieldValue('cars-fe-address-line1', parsed.line1); 4656 setFieldValue('cars-fe-address-line2', parsed.line2); 4657 setFieldValue('cars-fe-address-town', parsed.townCounty); 4658 setFieldValue('cars-fe-address-postcode-manual', parsed.postcode); 4659 if (!addressStep.dataset.carsFeManualWrapIntent) { 4660 addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_MANUAL_TOGGLE; 4661 } 4662 setManualAddressMode(true); 4663 const residency = document.getElementById('cars-fe-address-residency'); 4664 if (residency && typeof residency.focus === 'function') { 4665 try { 4666 residency.focus({ preventScroll: true }); 4667 } catch (err) { 4668 residency.focus(); 4669 } 4670 } 4671 }; 4672 4673 const buildLookupEntryFromDom = () => { 4674 const full = val('cars-fe-address-select'); 4675 const lookupPc = val('cars-fe-address-postcode').trim(); 4676 const parsed = parseLookupSelectedLineToEntryFields(full, lookupPc); 4677 return { 4678 id: newAddressEntryId(), 4679 source: 'lookup', 4680 selectedLine: full, 4681 lookupPostcode: lookupPc, 4682 line1: parsed.line1, 4683 line2: parsed.line2, 4684 townCounty: parsed.townCounty, 4685 postcode: parsed.postcode, 4686 residency: '', 4687 years: 0, 4688 months: 0, 4689 monthsAt: 0, 4690 timeSummary: '', 4691 }; 4692 }; 4693 4694 const buildManualEntryFromForm = (idOverride) => { 4695 const yStr = val('cars-fe-address-years-at'); 4696 const mStr = val('cars-fe-address-months-at'); 4697 const y = yStr === '' ? 0 : parseInt(yStr, 10); 4698 const m = mStr === '' ? 0 : parseInt(mStr, 10); 4699 const years = Number.isNaN(y) ? 0 : y; 4700 // 15+ years: months are not applicable â store 0. 4701 const months = years >= 15 ? 0 : Number.isNaN(m) ? 0 : m; 4702 const town = val('cars-fe-address-town'); 4703 const county = val('cars-fe-address-county'); 4704 const townCounty = county ? `${town}, ${county}` : town; 4705 return { 4706 id: typeof idOverride === 'string' && idOverride.length > 0 ? idOverride : newAddressEntryId(), 4707 source: 'manual', 4708 line1: val('cars-fe-address-line1'), 4709 line2: val('cars-fe-address-line2'), 4710 townCounty, 4711 postcode: val('cars-fe-address-postcode-manual'), 4712 residency: val('cars-fe-address-residency'), 4713 years, 4714 months, 4715 monthsAt: years >= 15 ? 15 * 12 : years * 12 + months, 4716 timeSummary: formatYearsMonthsAtSummary(String(years), String(months)), 4717 }; 4718 }; 4719 4720 const clearManualAddressFormFields = () => { 4721 [ 4722 'cars-fe-address-line1', 4723 'cars-fe-address-line2', 4724 'cars-fe-address-town', 4725 'cars-fe-address-postcode-manual', 4726 'cars-fe-address-county', 4727 'cars-fe-address-residency', 4728 'cars-fe-address-years-at', 4729 'cars-fe-address-months-at', 4730 ].forEach((id) => { 4731 const el = document.getElementById(id); 4732 if (el) { 4733 el.value = financeEligibilityFieldDefaultValue(id); 4734 } 4735 }); 4736 syncAllFinanceDurationSelectShells(); 4737 syncAddressMonthsDisabledForFifteenPlus(); 4738 }; 4739 4740 const applyManualEntryToForm = (entry) => { 4741 const setId = (id, v) => { 4742 const el = document.getElementById(id); 4743 if (el) { 4744 el.value = v != null ? String(v) : ''; 4745 } 4746 }; 4747 setId('cars-fe-address-line1', entry.line1); 4748 setId('cars-fe-address-line2', entry.line2); 4749 setId('cars-fe-address-postcode-manual', entry.postcode); 4750 setId('cars-fe-address-residency', entry.residency);
4751 setId('cars-fe-address-years-at', String(entry.years)); 4752 setId('cars-fe-address-months-at', entry.years >= 15 ? '0' : String(entry.months)); 4753 const townCounty = entry.townCounty || ''; 4754 const commaIdx = townCounty.indexOf(','); 4755 if (commaIdx > 0) { 4756 setId('cars-fe-address-town', townCounty.slice(0, commaIdx).trim()); 4757 setId('cars-fe-address-county', townCounty.slice(commaIdx + 1).trim()); 4758 } else { 4759 setId('cars-fe-address-town', townCounty); 4760 setId('cars-fe-address-county', ''); 4761 } 4762 syncAllFinanceDurationSelectShells(); 4763 syncAddressMonthsDisabledForFifteenPlus(); 4764 }; 4765 4766 const placeAddressManualWrapHome = () => { 4767 const manualWrap = document.getElementById('cars-fe-address-manual-wrap'); 4768 const home = document.querySelector('#cars-fe-step-address .cars-fe-form-stack--address'); 4769 if (!manualWrap || !home) { 4770 return; 4771 } 4772 const entryBlock = document.getElementById('cars-fe-address-entry-block'); 4773 if (entryBlock && entryBlock.parentElement === home) { 4774 // Only move when not already immediately after the entry block â reparenting steals input focus. 4775 if (manualWrap.previousElementSibling !== entryBlock || manualWrap.parentElement !== home) { 4776 entryBlock.after(manualWrap); 4777 } 4778 return; 4779 } 4780 if (manualWrap.parentElement !== home) { 4781 home.appendChild(manualWrap); 4782 } 4783 }; 4784 4785 const placeAddressManualWrapForEdit = () => { 4786 const manualWrap = document.getElementById('cars-fe-address-manual-wrap'); 4787 const slot = document.getElementById('cars-fe-address-edit-slot'); 4788 if (!manualWrap) { 4789 return; 4790 } 4791 if (editingEntryId != null && slot) { 4792 if (manualWrap.parentElement !== slot) { 4793 slot.appendChild(manualWrap); 4794 } 4795 return; 4796 } 4797 placeAddressManualWrapHome(); 4798 }; 4799 4800 const getSavedAddressCardsRenderKey = () => 4801 `${editingEntryId || ''}|${savedAddressEntries.map((e) => `${e.id}:${e.source}:${e.timeSummary || ''}:${e.line1 || ''}:${e.postcode || ''}`).join(';')}`; 4802 4803 let lastSavedAddressCardsRenderKey = null; 4804 4805 const renderSavedAddressCards = (force = false) => { 4806 const mount = document.getElementById('cars-fe-address-saved-cards'); 4807 if (!mount) { 4808 return; 4809 } 4810 const nextKey = getSavedAddressCardsRenderKey(); 4811 if (!force && nextKey === lastSavedAddressCardsRenderKey) { 4812 placeAddressManualWrapForEdit(); 4813 return; 4814 } 4815 lastSavedAddressCardsRenderKey = nextKey; 4816 // Moving the edit form into a card slot â pull it out before clearing so we don't destroy it. 4817 if (mount.contains(document.getElementById('cars-fe-address-manual-wrap'))) { 4818 placeAddressManualWrapHome(); 4819 } 4820 mount.textContent = ''; 4821 savedAddressEntries.forEach((entry, index) => { 4822 if (editingEntryId && entry.id === editingEntryId) { 4823 const slot = document.createElement('div'); 4824 slot.id = 'cars-fe-address-edit-slot'; 4825 slot.className = 'cars-fe-address-edit-slot'; 4826 slot.setAttribute( 4827 'aria-label', 4828 index === 0 ? 'Editing current address' : 'Editing previous address', 4829 ); 4830 mount.appendChild(slot); 4831 return; 4832 } 4833 const card = document.createElement('article'); 4834 card.className = `cars-fe-address-current-summary-card${entry.source === 'lookup' ? ' cars-fe-address-current-summary-card--lookup' : ''}`; 4835 const header = document.createElement('div'); 4836 header.className = 'cars-fe-address-current-summary-card__header'; 4837 const title = document.createElement('p'); 4838 title.className = 'cars-fe-address-current-summary-card__title'; 4839 title.textContent = index === 0 ? 'Current address' : 'Previous address'; 4840 const editBtn = document.createElement('button'); 4841 editBtn.type = 'button'; 4842 editBtn.className = 'cars-fe-address-saved-edit-button'; 4843 editBtn.setAttribute('aria-label', 'Edit address'); 4844 editBtn.setAttribute('data-cars-fe-entry-edit', entry.id); 4845 const icon = document.createElement('span'); 4846 icon.className = 'cars-fe-address-saved-edit-button__icon'; 4847 editBtn.appendChild(icon); 4848 header.appendChild(title); 4849 header.appendChild(editBtn); 4850 card.appendChild(header); 4851 const rule = document.createElement('div'); 4852 rule.className = 'cars-fe-address-current-summary-card__rule'; 4853 card.appendChild(rule); 4854 const columns = document.createElement('div'); 4855 columns.className = 'cars-fe-address-current-summary-card__columns'; 4856 const colAddr = document.createElement('div'); 4857 colAddr.className = 4858 'cars-fe-address-current-summary-card__col cars-fe-address-current-summary-card__col--address'; 4859 const addrLabelRow = document.createElement('div'); 4860 addrLabelRow.className = 'cars-fe-address-current-summary-card__field-label-row'; 4861 const addrLabel = document.createElement('p'); 4862 addrLabel.className = 'cars-fe-address-current-summary-card__field-label'; 4863 addrLabel.textContent = 'Address'; 4864 addrLabelRow.appendChild(addrLabel); 4865 colAddr.appendChild(addrLabelRow); 4866 const lines = document.createElement('div'); 4867 lines.className = 'cars-fe-saved-address-lines'; 4868 const p1 = document.createElement('p'); 4869 p1.className = 'cars-fe-saved-address-line'; 4870 p1.textContent = entry.line1 || ''; 4871 lines.appendChild(p1); 4872 if (entry.line2) { 4873 const p2 = document.createElement('p'); 4874 p2.className = 'cars-fe-saved-address-line'; 4875 p2.textContent = entry.line2; 4876 lines.appendChild(p2); 4877 } 4878 if (entry.townCounty) { 4879 const pt = document.createElement('p'); 4880 pt.className = 'cars-fe-saved-address-line'; 4881 pt.textContent = entry.townCounty; 4882 lines.appendChild(pt); 4883 }
4884 const ppc = document.createElement('p'); 4885 ppc.className = 'cars-fe-saved-address-line'; 4886 ppc.textContent = entry.postcode || ''; 4887 lines.appendChild(ppc); 4888 colAddr.appendChild(lines); 4889 columns.appendChild(colAddr); 4890 const colMeta = document.createElement('div'); 4891 colMeta.className = 4892 'cars-fe-address-current-summary-card__col cars-fe-address-current-summary-card__col--meta'; 4893 const resBlock = document.createElement('div'); 4894 resBlock.className = 'cars-fe-address-current-summary-card__meta-block'; 4895 const resLabelRow = document.createElement('div'); 4896 resLabelRow.className = 'cars-fe-address-current-summary-card__field-label-row'; 4897 const resLab = document.createElement('p'); 4898 resLab.className = 'cars-fe-address-current-summary-card__field-label'; 4899 resLab.textContent = 'Residency type'; 4900 resLabelRow.appendChild(resLab); 4901 resBlock.appendChild(resLabelRow); 4902 const resVal = document.createElement('p'); 4903 resVal.className = 'cars-fe-saved-meta-value'; 4904 resVal.textContent = entry.residency || ''; 4905 resBlock.appendChild(resVal); 4906 colMeta.appendChild(resBlock); 4907 const timeBlock = document.createElement('div'); 4908 timeBlock.className = 'cars-fe-address-current-summary-card__meta-block'; 4909 const timeLabelRow = document.createElement('div'); 4910 timeLabelRow.className = 'cars-fe-address-current-summary-card__field-label-row'; 4911 const timeLab = document.createElement('p'); 4912 timeLab.className = 'cars-fe-address-current-summary-card__field-label'; 4913 timeLab.textContent = 'Time at this address'; 4914 timeLabelRow.appendChild(timeLab); 4915 timeBlock.appendChild(timeLabelRow); 4916 const timeVal = document.createElement('p'); 4917 timeVal.className = 'cars-fe-saved-meta-value'; 4918 timeVal.textContent = entry.timeSummary || ''; 4919 timeBlock.appendChild(timeVal); 4920 colMeta.appendChild(timeBlock); 4921 columns.appendChild(colMeta); 4922 card.appendChild(columns); 4923 mount.appendChild(card); 4924 }); 4925 placeAddressManualWrapForEdit(); 4926 }; 4927 4928 const populateAddressSelect = (lines) => { 4929 const selectEl = document.getElementById('cars-fe-address-select'); 4930 if (!selectEl) { 4931 return; 4932 } 4933 selectEl.innerHTML = ''; 4934 const ph = document.createElement('option'); 4935 ph.value = ''; 4936 ph.textContent = 'Select an address'; 4937 selectEl.appendChild(ph); 4938 (lines || []).forEach((line) => { 4939 const o = document.createElement('option'); 4940 o.value = line; 4941 o.textContent = line; 4942 selectEl.appendChild(o); 4943 }); 4944 }; 4945 4946 const MANUAL_WRAP_INTENT_MANUAL_TOGGLE = 'manual-toggle'; 4947 const MANUAL_WRAP_INTENT_ADD_ANOTHER = 'add-another'; 4948 4949 const addressEntryFlowShouldShow = () => { 4950 if (!addressStep) { 4951 return true; 4952 } 4953 if (savedAddressEntries.length === 0) { 4954 return true; 4955 } 4956 return String(addressStep.dataset.carsFeAddressEntryActive || '') === 'true'; 4957 }; 4958 4959 const normalizeUkPostcodeKey = (pc) => String(pc || '').toUpperCase().replace(/\s+/g, ''); 4960 4961 const setManualAddressMode = (manual) => { 4962 if (!addressStep) { 4963 return; 4964 } 4965 if (manual) { 4966 addressStep.classList.add('cars-fe-address--manual'); 4967 } else { 4968 addressStep.classList.remove('cars-fe-address--manual'); 4969 delete addressStep.dataset.carsFeManualWrapIntent; 4970 } 4971 validateAddress(); 4972 }; 4973 4974 const validateManualFormForSave = () => 4975 Boolean(val('cars-fe-address-line1')) && 4976 Boolean(val('cars-fe-address-town')) && 4977 ukPostcodeLooksComplete(val('cars-fe-address-postcode-manual')) && 4978 Boolean(val('cars-fe-address-residency')) && 4979 durationYearsMonthsOk('cars-fe-address-years-at', 'cars-fe-address-months-at', { 4980 plusYearsValue: 15, 4981 }); 4982 4983 const syncLookupEntryFromSelectIfApplicable = () => { 4984 if (!addressStep) { 4985 return; 4986 } 4987 const manualExpanded = addressStep.classList.contains('cars-fe-address--manual'); 4988 const selectEl = document.getElementById('cars-fe-address-select'); 4989 const selectedLine = selectEl ? String(selectEl.value || '').trim() : ''; 4990 const selectComplete = 4991 ukPostcodeLooksComplete(val('cars-fe-address-postcode')) && Boolean(selectedLine); 4992 const entryFlowActive = String(addressStep.dataset.carsFeAddressEntryActive || '') === 'true'; 4993 4994 if (!selectComplete || manualExpanded) { 4995 if ( 4996 savedAddressEntries.length === 1 && 4997 savedAddressEntries[0].source === 'lookup' && 4998 !selectComplete 4999 ) { 5000 savedAddressEntries = []; 5001 } 5002 return; 5003 } 5004 5005 const next = buildLookupEntryFromDom(); 5006 5007 if (savedAddressEntries.length === 0) { 5008 savedAddressEntries = [next]; 5009 return; 5010 } 5011 5012 if (entryFlowActive && savedAddressEntries.length >= 1) { 5013 const last = savedAddressEntries[savedAddressEntries.length - 1]; 5014 if ( 5015 last.source === 'lookup' && 5016 last.selectedLine === next.selectedLine && 5017 normalizeUkPostcodeKey(last.lookupPostcode) === normalizeUkPostcodeKey(next.lookupPostcode) 5018 ) { 5019 return; 5020 } 5021 savedAddressEntries.push(next); 5022 delete addressStep.dataset.carsFeAddressEntryActive; 5023 const pc = document.getElementById('cars-fe-address-postcode'); 5024 if (pc) { 5025 pc.value = ''; 5026 } 5027 populateAddressSelect([]); 5028 addressStep.classList.remove('cars-fe-address--manual'); 5029 delete addressStep.dataset.carsFeManualWrapIntent; 5030 return; 5031 } 5032 5033 const existingId = 5034 savedAddressEntries.length > 0 && savedAddressEntries[0].source === 'lookup' 5035 ? savedAddressEntries[0].id 5036 : null; 5037 if (existingId) { 5038 next.id = existingId; 5039 } 5040 if (savedAddressEntries[0].source === 'lookup') { 5041 savedAddressEntries[0] = next; 5042 } 5043 }; 5044 5045 const syncAddressSummaryUiState = () => { 5046 const savedSummaryPanel = document.getElementById('cars-fe-address-saved-summary-panel'); 5047 if (!addressStep || addressStep.hidden) { 5048 if (savedSummaryPanel) { 5049 savedSummaryPanel.hidden = true; 5050 } 5051 renderSavedAddressCards(); 5052 return; 5053 } 5054 syncLookupEntryFromSelectIfApplicable(); 5055 5056 const manualWrap = document.getElementById('cars-fe-address-manual-wrap'); 5057 const manualExpanded = addressStep.classList.contains('cars-fe-address--manual'); 5058 const inEdit = editingEntryId != null && editingInsertIndex != null; 5059 const hasSaved = 5060 savedAddressEntries.length > 0 || 5061 inEdit; 5062 const intent = addressStep.dataset.carsFeManualWrapIntent || ''; 5063 5064 if (manualWrap) { 5065 let showManualWrap = false;
5066 if (inEdit) { 5067 // Editing a saved address: always show the form in that address's slot. 5068 showManualWrap = true; 5069 } else if (!hasSaved) { 5070 showManualWrap = 5071 manualExpanded && 5072 (intent === MANUAL_WRAP_INTENT_MANUAL_TOGGLE || intent === MANUAL_WRAP_INTENT_ADD_ANOTHER); 5073 } else if (intent === MANUAL_WRAP_INTENT_MANUAL_TOGGLE) { 5074 showManualWrap = manualExpanded; 5075 } else if (intent === MANUAL_WRAP_INTENT_ADD_ANOTHER) { 5076 showManualWrap = manualExpanded && savedAddressEntries.length >= 1; 5077 } 5078 manualWrap.hidden = !showManualWrap; 5079 } 5080 5081 // While editing a saved address, hide lookup/add flow so only the in-place form is shown. 5082 const entryFlowVisible = inEdit ? false : addressEntryFlowShouldShow(); 5083 const entryBlock = document.getElementById('cars-fe-address-entry-block'); 5084 if (entryBlock) { 5085 entryBlock.hidden = !entryFlowVisible; 5086 } 5087 addressStep.classList.toggle('cars-fe-address--entry-flow-visible', entryFlowVisible); 5088 addressStep.classList.toggle('cars-fe-address--editing-saved', inEdit); 5089 5090 const deleteBtn = document.getElementById('cars-fe-address-manual-delete-button'); 5091 if (deleteBtn) { 5092 deleteBtn.hidden = !inEdit; 5093 } 5094 5095 const addAnotherCardEl = document.querySelector( 5096 '#cars-fe-address-saved-summary-panel .cars-fe-address-add-another-card' 5097 ); 5098 if (addAnotherCardEl) { 5099 const historyComplete = getTotalAddressHistoryMonthsFromEntries() >= 36; 5100 const hideWhileAddingAnother = 5101 savedAddressEntries.length > 0 && entryFlowVisible; 5102 addAnotherCardEl.hidden = !hasSaved || historyComplete || hideWhileAddingAnother || inEdit; 5103 } 5104 5105 addressStep.classList.toggle('cars-fe-address--has-saved-summary', hasSaved); 5106 const anyManual = savedAddressEntries.some((e) => e.source === 'manual'); 5107 const anyLookup = savedAddressEntries.some((e) => e.source === 'lookup'); 5108 addressStep.classList.toggle('cars-fe-address--manual-saved', anyManual); 5109 addressStep.classList.toggle('cars-fe-address--lookup-saved', anyLookup && !anyManual); 5110 5111 renderSavedAddressCards(); 5112 5113 if (savedSummaryPanel) { 5114 savedSummaryPanel.hidden = !hasSaved; 5115 } 5116 5117 syncAddressSelectBlockVisibility(); 5118 }; 5119 5120 const runAddressLookup = () => { 5121 if (!addressStep || addressStep.hidden) { 5122 return; 5123 } 5124 const pc = val('cars-fe-address-postcode'); 5125 if (!ukPostcodeLooksComplete(pc)) { 5126 populateAddressSelect([]); 5127 validateAddress(); 5128 return; 5129 } 5130 if (typeof API === 'undefined' || !API.Stock || typeof API.Stock.LookupUkPostcodeAddresses !== 'function') { 5131 validateAddress(); 5132 return; 5133 } 5134 API.Stock.LookupUkPostcodeAddresses(pc) 5135 .then((res) => { 5136 const lines = res.Addresses || res.addresses || []; 5137 populateAddressSelect(Array.isArray(lines) ? lines : []); 5138 validateAddress(); 5139 }) 5140 .catch(() => { 5141 populateAddressSelect([]); 5142 validateAddress(); 5143 }); 5144 }; 5145 5146 const applyAddressFieldHighlights = () => { 5147 if (!addressStep) { 5148 return; 5149 } 5150 const manualWrap = document.getElementById('cars-fe-address-manual-wrap'); 5151 const midEdit = editingEntryId != null && editingInsertIndex != null; 5152 const manualVisible = (manualWrap && manualWrap.hidden === false) || midEdit; 5153 const addressFieldIds = [ 5154 'cars-fe-address-line1', 5155 'cars-fe-address-town', 5156 'cars-fe-address-postcode-manual', 5157 'cars-fe-address-residency', 5158 'cars-fe-address-years-at', 5159 'cars-fe-address-months-at', 5160 'cars-fe-address-postcode', 5161 'cars-fe-address-select', 5162 ]; 5163 const mark = feStepFieldMarker('address'); 5164 if (manualVisible) { 5165 addressFieldIds.forEach((id) =>
5165 financeEligibilityMarkFieldInvalid(id, false)); 5166 mark('cars-fe-address-line1', !val('cars-fe-address-line1')); 5167 mark('cars-fe-address-town', !val('cars-fe-address-town')); 5168 syncLiveManualUkPostcodeFeedback(); 5169 mark('cars-fe-address-residency', !val('cars-fe-address-residency')); 5170 const durationInvalid = !durationYearsMonthsOk( 5171 'cars-fe-address-years-at', 5172 'cars-fe-address-months-at', 5173 { plusYearsValue: 15 }, 5174 ); 5175 mark('cars-fe-address-years-at', durationInvalid); 5176 mark('cars-fe-address-months-at', durationInvalid); 5177 return; 5178 } 5179 addressFieldIds.forEach((id) => financeEligibilityMarkFieldInvalid(id, false)); 5180 const entryBlock = document.getElementById('cars-fe-address-entry-block'); 5181 if (!entryBlock || entryBlock.hidden) { 5182 return; 5183 } 5184 const selectElAddr = document.getElementById('cars-fe-address-select'); 5185 const selectedLineAddr = selectElAddr ? String(selectElAddr.value || '').trim() : ''; 5186 const postcoderActive = typeof PostcoderAutocomplete === 'function'; 5187 // Legacy select path still needs a UK postcode; Postcoder search accepts address text. 5188 const selectComplete = postcoderActive 5189 ? false 5190 : ukPostcodeLooksComplete(val('cars-fe-address-postcode')) && Boolean(selectedLineAddr); 5191 mark( 5192 'cars-fe-address-postcode', 5193 postcoderActive 5194 ? false 5195 : !ukPostcodeLooksComplete(val('cars-fe-address-postcode')), 5196 ); 5197 mark('cars-fe-address-select', !selectComplete); 5198 }; 5199 5200 const validateAddress = () => { 5201 if (!addressNextBtn) { 5202 return false; 5203 } 5204 if (!addressStep || addressStep.hidden) { 5205 setBtnNextDisabled(addressNextBtn, true); 5206 return false; 5207 } 5208 syncAddressSummaryUiState(); 5209 5210 const hasSavedForNext = savedAddressEntries.length > 0; 5211 const midEdit = editingEntryId != null && editingInsertIndex != null; 5212 5213 // 3 years of address history is required whatever route added the entries. A Postcoder 5214 // selection opens the manual card for residency and time at address, so it carries a 5215 // duration; the legacy select fallback does not, and must not skip the rule. 5216 const historyInsufficient = getTotalAddressHistoryMonthsFromEntries() < 36; 5217 5218 const ok = hasSavedForNext && !historyInsufficient && !midEdit; 5219 5220 applyAddressFieldHighlights(); 5221 setBtnNextDisabled(addressNextBtn, !ok); 5222 syncAddressHistoryAlert(); 5223 return ok; 5224 }; 5225 5226 const hideContactStep = () => { 5227 if (contactStep) { 5228 contactStep.hidden = true; 5229 } 5230 }; 5231 5232 const hideMoreStep = () => { 5233 if (moreStep) { 5234 moreStep.hidden = true; 5235 } 5236 }; 5237 5238 const hidePersonalGroupedStep = () => { 5239 if (personalGroupedStep) { 5240 personalGroupedStep.hidden = true; 5241 } 5242 }; 5243 5244 const hideIncomeOutgoingsGroupedStep = () => { 5245 if (incomeOutgoingsGroupedStep) { 5246 incomeOutgoingsGroupedStep.hidden = true; 5247 } 5248 }; 5249 5250 const cancelUnsavedAddressEdit = () => { 5251 const wasEditing = editingEntryId != null || editingInsertIndex != null; 5252 const wasAdding = 5253 addressStep && 5254 String(addressStep.dataset.carsFeAddressEntryActive || '') === 'true' && 5255 !wasEditing; 5256 if (!wasEditing && !wasAdding) { 5257 return; 5258 } 5259 editingEntryId = null; 5260 editingInsertIndex = null; 5261 clearManualAddressFormFields(); 5262 placeAddressManualWrapHome(); 5263 addressStep?.classList.remove('cars-fe-address--manual', 'cars-fe-address--editing-saved'); 5264 if (addressStep) { 5265 delete addressStep.dataset.carsFeManualWrapIntent; 5266 delete addressStep.dataset.carsFeAddressEntryActive; 5267 } 5268 const manualWrap = document.getElementById('cars-fe-address-manual-wrap'); 5269 if (manualWrap) { 5270 manualWrap.hidden = true; 5271 } 5272 }; 5273 5274 const hideAddressStep = () => { 5275 if (addressStep) { 5276 cancelUnsavedAddressEdit(); 5277 addressStep.classList.remove('cars-fe-address-suggestions-open', 'cars-fe-duration-dropdown-open'); 5278 addressStep.hidden = true; 5279 } 5280 }; 5281 5282 const hideEmploymentStep = () => { 5283 if (employmentStep) { 5284 employmentStep.hidden = true; 5285 } 5286 }; 5287 5288 const hideIncomeOutgoingsStep = () => { 5289 if (incomeOutgoingsStep) { 5290 incomeOutgoingsStep.hidden = true; 5291 } 5292 }; 5293 5294 const hideOutgoingsFinalStep = () => { 5295 if (outgoingsFinalStep) { 5296 outgoingsFinalStep.hidden = true; 5297 } 5298 }; 5299 5300 const hideReviewStep = () => { 5301 if (reviewStep) { 5302 reviewStep.hidden = true; 5303 } 5304 }; 5305 5306 const setReviewText = (id, text) => { 5307 const el = document.getElementById(id); 5308 if (!el) { 5309 return; 5310 } 5311 const s = text != null ? String(text).trim() : ''; 5312 el.textContent = s === '' ? 'â' : s; 5313 }; 5314 5315 const setReviewRowHidden = (rowId, visible) => {
5316 const row = document.getElementById(rowId); 5317 if (row) { 5318 row.hidden = !visible; 5319 } 5320 }; 5321 5322 const formatPhoneForReview = (dial, national) => { 5323 const d = String(dial || '').trim(); 5324 const n = String(national || '').trim(); 5325 if (!d && !n) { 5326 return ''; 5327 } 5328 if (d && n) { 5329 return `+${d.replace(/^\+/, '')} ${n}`; 5330 } 5331 return d || n; 5332 }; 5333 5334 const formatAddressEntryLines = (entry) => { 5335 if (!entry) { 5336 return ''; 5337 } 5338 if (entry.source === 'lookup') { 5339 const line = (entry.selectedLine || '').trim(); 5340 if (line) { 5341 return line; 5342 } 5343 } 5344 const bits = [entry.line1, entry.line2, entry.townCounty, entry.postcode] 5345 .map((x) => (x != null ? String(x).trim() : '')) 5346 .filter(Boolean); 5347 return bits.join('\n'); 5348 }; 5349 5350 const appendReviewSummaryKvRow = (parent, labelText, valueText) => { 5351 const row = document.createElement('div'); 5352 row.className = 'cars-fe-review-kv'; 5353 const lab = document.createElement('div'); 5354 lab.className = 'cars-fe-review-kv__label'; 5355 lab.textContent = labelText; 5356 const valCell = document.createElement('div'); 5357 valCell.className = 'cars-fe-review-kv__value'; 5358 const span = document.createElement('span'); 5359 const v = valueText != null ? String(valueText).trim() : ''; 5360 span.textContent = v === '' ? 'â' : v; 5361 valCell.appendChild(span); 5362 const rule = document.createElement('div'); 5363 rule.className = 'cars-fe-review-kv__rule'; 5364 row.appendChild(lab); 5365 row.appendChild(valCell); 5366 row.appendChild(rule); 5367 parent.appendChild(row); 5368 }; 5369 5370 const populateFinanceEligibilityReview = () => { 5371 const p = collectJourneyDetailPayload(); 5372 5373 setReviewText('cars-fe-review-personal-title', p.title); 5374 setReviewText('cars-fe-review-personal-first', p.firstName); 5375 setReviewText('cars-fe-review-personal-last', p.lastName); 5376 setReviewText('cars-fe-review-personal-email', p.email); 5377 setReviewText('cars-fe-review-personal-phone', formatPhoneForReview(p.phoneDial, p.phoneNational)); 5378 setReviewText('cars-fe-review-personal-dob', p.dateOfBirth); 5379 setReviewText('cars-fe-review-personal-license', p.licenseType); 5380 5381 setReviewText('cars-fe-review-other-marital', p.maritalStatus); 5382 setReviewText('cars-fe-review-other-dependants', p.numberOfDependants); 5383 5384 const addressMount = document.getElementById('cars-fe-review-address-mount'); 5385 if (addressMount) { 5386 addressMount.textContent = ''; 5387 savedAddressEntries.forEach((entry, index) => { 5388 const lines = formatAddressEntryLines(entry); 5389 if (!lines) { 5390 return; 5391 } 5392 const card = document.createElement('div'); 5393 card.className = 'cars-fe-review-summary-card'; 5394 if (savedAddressEntries.length > 1) { 5395 appendReviewSummaryKvRow( 5396 card, 5397 'Address period:', 5398 index === 0 ? 'Current' : 'Previous', 5399 ); 5400 } 5401 appendReviewSummaryKvRow(card, 'Address:', lines); 5402 appendReviewSummaryKvRow( 5403 card, 5404 'Residency:', 5405 (entry.residency && String(entry.residency).trim()) || 'â', 5406 ); 5407 const duration = 5408 entry.timeSummary || 5409 formatYearsMonthsAtSummary(String(entry.years), String(entry.months)) || 5410 'â'; 5411 appendReviewSummaryKvRow(card, 'Time at Address:', duration); 5412 addressMount.appendChild(card); 5413 }); 5414 } 5415 5416 const empMount = document.getElementById('cars-fe-review-employment-mount'); 5417 if (empMount) { 5418 empMount.textContent = '';
5419 savedEmploymentEntries.forEach((entry, empIndex) => { 5420 const card = document.createElement('div'); 5421 card.className = 'cars-fe-review-summary-card'; 5422 const categoryValue = 5423 (entry.occupationalType && String(entry.occupationalType).trim()) || 5424 (entry.occupationalBasis && String(entry.occupationalBasis).trim()) || 5425 'â'; 5426 if (savedEmploymentEntries.length > 1) { 5427 appendReviewSummaryKvRow( 5428 card, 5429 'Employment period:', 5430 empIndex === 0 ? 'Current' : 'Previous', 5431 ); 5432 } 5433 appendReviewSummaryKvRow(card, 'Employment Status:', entry.status || 'â'); 5434 appendReviewSummaryKvRow(card, 'Employers name:', entry.employerName || 'â'); 5435 appendReviewSummaryKvRow(card, 'Occupation:', entry.occupation || 'â'); 5436 appendReviewSummaryKvRow(card, 'Employment category:', categoryValue); 5437 appendReviewSummaryKvRow( 5438 card, 5439 'Time with employer:', 5440 formatEmploymentDurationSummary(entry.years, entry.months), 5441 ); 5442 empMount.appendChild(card); 5443 }); 5444 } 5445 5446 setReviewText( 5447 'cars-fe-review-income-annual', 5448 formatSterlingForReview(p.totalAnnualIncomeBeforeTax) || p.totalAnnualIncomeBeforeTax, 5449 ); 5450 setReviewText( 5451 'cars-fe-review-income-takehome', 5452 formatSterlingForReview(p.typicalMonthlyTakeHomePay) || p.typicalMonthlyTakeHomePay, 5453 ); 5454 setReviewText( 5455 'cars-fe-review-income-household', 5456 formatSterlingForReview(p.otherAnnualHouseholdIncome) || p.otherAnnualHouseholdIncome, 5457 ); 5458 setReviewText('cars-fe-review-income-repay', p.repayExistingVehicleFinanceAgreement); 5459 setReviewText( 5460 'cars-fe-review-income-mortgage', 5461 formatSterlingForReview(p.monthlyMortgageRentShare) || p.monthlyMortgageRentShare, 5462 ); 5463 setReviewText( 5464 'cars-fe-review-income-bills', 5465 formatSterlingForReview(p.otherTotalMonthlyBills) || p.otherTotalMonthlyBills, 5466 ); 5467 5468 const repayYes = p.repayExistingVehicleFinanceAgreement === 'Yes'; 5469 setReviewRowHidden('cars-fe-review-row-existing-loan', repayYes); 5470 if (repayYes) { 5471 setReviewText( 5472 'cars-fe-review-income-existing-loan', 5473 formatSterlingForReview(p.existingVehicleLoanMonthlyPayment) || 5474 p.existingVehicleLoanMonthlyPayment, 5475 ); 5476 } 5477 5478 const downturnYes = p.foreseeFinancialDownturn === 'Yes'; 5479 setReviewRowHidden('cars-fe-review-row-downturn-reason', downturnYes); 5480 setReviewRowHidden('cars-fe-review-row-downturn-income', downturnYes); 5481 setReviewText('cars-fe-review-income-downturn-yn', p.foreseeFinancialDownturn); 5482 if (downturnYes) { 5483 setReviewText('cars-fe-review-income-downturn-reason', p.expectedDownturnReason); 5484 setReviewText( 5485 'cars-fe-review-income-downturn-reduced', 5486 formatSterlingForReview(p.reducedAnnualIncomeBeforeTax) || p.reducedAnnualIncomeBeforeTax, 5487 ); 5488 } 5489 }; 5490 5491 const syncReviewConsentError = (invalid) => { 5492 const row = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__row'); 5493 const card = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__card'); 5494 const err = document.getElementById('cars-fe-review-consent-error'); 5495 const show = Boolean(invalid); 5496 if (row) { 5497 row.classList.toggle('cars-fe-review-consent__row--invalid', show); 5498 } 5499 if (card) { 5500 card.classList.toggle('cars-fe-review-consent__card--invalid', show); 5501 } 5502 if (err) { 5503 err.hidden = !show; 5504 err.setAttribute('aria-hidden', show ? 'false' : 'true'); 5505 } 5506 }; 5507 5508 const validateReviewConsent = () => { 5509 if (!reviewViewResultsBtn || !reviewStep || reviewStep.hidden) { 5510 return false;
5511 } 5512 const ok = Boolean(reviewConsentCb && reviewConsentCb.checked); 5513 setBtnNextDisabled(reviewViewResultsBtn, !ok); 5514 if (ok) { 5515 syncReviewConsentError(false); 5516 } 5517 return ok; 5518 }; 5519 5520 const prepareEditFromReview = () => { 5521 hideReviewStep(); 5522 if (intro) { 5523 intro.hidden = true; 5524 } 5525 hideAgreementView(); 5526 hideContactStep(); 5527 hideMoreStep(); 5528 hidePersonalGroupedStep(); 5529 hideAddressStep(); 5530 hideEmploymentStep(); 5531 hideIncomeOutgoingsStep(); 5532 hideOutgoingsFinalStep(); 5533 hideIncomeOutgoingsGroupedStep(); 5534 if (detailsStep) { 5535 detailsStep.hidden = true; 5536 } 5537 }; 5538 5539 const jumpToReviewEdit = (target) => { 5540 const key = String(target || '').trim(); 5541 if (!key) { 5542 return; 5543 } 5544 prepareEditFromReview(); 5545 reviewEditReturnPending = true; 5546 state.dirty = true; 5547 if ((key === 'details' || key === 'more') && canRouteToGroupedSteps(personalGroupedStep)) { 5548 showPersonalGroupedStep(); 5549 return; 5550 } 5551 if (key === 'details' && detailsStep) { 5552 detailsStep.hidden = false; 5553 state.screen = 'details'; 5554 scrollToTop(detailsStep); 5555 validateDetails(); 5556 syncFinanceEligibilityJourneyUrl(); 5557 return; 5558 } 5559 if (key === 'more' && moreStep && contactStep) { 5560 contactStep.hidden = true; 5561 moreStep.hidden = false; 5562 state.screen = 'more'; 5563 scrollToTop(moreStep); 5564 validateMore(); 5565 syncFinanceEligibilityJourneyUrl(); 5566 return; 5567 } 5568 if (key === 'address') { 5569 showAddressStep(); 5570 return; 5571 } 5572 if ((key === 'employment' || key === 'income') && canRouteToGroupedSteps(incomeOutgoingsGroupedStep)) { 5573 showIncomeOutgoingsGroupedStep(); 5574 return; 5575 } 5576 if (key === 'employment') { 5577 showEmploymentStep(); 5578 return; 5579 } 5580 if (key === 'income') { 5581 showIncomeOutgoingsStep(); 5582 return; 5583 } 5584 }; 5585 5586 const showReviewStep = () => { 5587 if (!reviewStep) { 5588 return; 5589 } 5590 clearReviewEditReturnPending(); 5591 hideAgreementView(); 5592 if (intro) { 5593 intro.hidden = true; 5594 } 5595 if (detailsStep) { 5596 detailsStep.hidden = true; 5597 } 5598 hideContactStep(); 5599 hideMoreStep(); 5600 hidePersonalGroupedStep(); 5601 hideAddressStep(); 5602 hideEmploymentStep(); 5603 hideIncomeOutgoingsStep(); 5604 hideOutgoingsFinalStep(); 5605 hideIncomeOutgoingsGroupedStep(); 5606 reviewStep.hidden = false; 5607 if (reviewConsentCb) { 5608 reviewConsentCb.checked = false; 5609 } 5610 syncReviewConsentError(false); 5611 populateFinanceEligibilityReview(); 5612 syncReviewConsentLenderName(); 5613 validateReviewConsent(); 5614 state.screen = 'review'; 5615 state.dirty = true; 5616 scrollToTop(reviewStep); 5617 syncFinanceEligibilityJourneyUrl(); 5618 }; 5619 5620 const collectJourneyDetailPayload = () => { 5621 const lookupEntry = savedAddressEntries.find((x) => x.source === 'lookup'); 5622 const flatManual = [...savedAddressEntries].reverse().find((x) => x.source === 'manual'); 5623 const useManual = Boolean(flatManual); 5624 const phoneCountry = val('cars-fe-phone-country'); 5625 const phoneDial = val('cars-fe-phone-dial'); 5626 const phoneNationalRaw = val('cars-fe-phone-national'); 5627 return { 5628 title: val('cars-fe-title'), 5629 firstName: val('cars-fe-first-name'), 5630 lastName: val('cars-fe-last-name'), 5631 dateOfBirth: formatIsoDobToUk(val('cars-fe-dob')), 5632 maritalStatus: val('cars-fe-marital'), 5633 email: val('cars-fe-email'), 5634 phoneDial, 5635 phoneCountry, 5636 phoneNational: normalizeUkNationalForPayload(phoneCountry, phoneDial, phoneNationalRaw), 5637 licenseType: val('cars-fe-license-type'),
5638 numberOfDependants: val('cars-fe-dependants'), 5639 addressLookupPostcode: val('cars-fe-address-postcode'), 5640 addressSelectedLine: lookupEntry ? lookupEntry.selectedLine : val('cars-fe-address-select'), 5641 addressManual: useManual, 5642 addressLine1: flatManual ? flatManual.line1 : '', 5643 addressLine2: flatManual ? flatManual.line2 : '', 5644 addressTown: flatManual 5645 ? flatManual.townCounty.includes(',') 5646 ? flatManual.townCounty.split(',')[0].trim() 5647 : flatManual.townCounty.trim() 5648 : '', 5649 addressCounty: 5650 flatManual && flatManual.townCounty.includes(',') 5651 ? flatManual.townCounty.split(',').slice(1).join(',').trim() 5652 : '', 5653 addressPostcode: flatManual ? flatManual.postcode : '', 5654 addressResidencyType: flatManual ? flatManual.residency : '', 5655 addressYearsAt: flatManual ? String(flatManual.years) : '', 5656 addressMonthsAt: flatManual ? String(flatManual.months) : '', 5657 employmentStatus: savedEmploymentEntries[0] ? savedEmploymentEntries[0].status : '', 5658 employmentEmployerName: savedEmploymentEntries[0] ? savedEmploymentEntries[0].employerName : '', 5659 employmentOccupation: savedEmploymentEntries[0] ? savedEmploymentEntries[0].occupation : '', 5660 employmentOccupationalBasis: savedEmploymentEntries[0] ? savedEmploymentEntries[0].occupationalBasis : '', 5661 employmentOccupationalType: savedEmploymentEntries[0] ? savedEmploymentEntries[0].occupationalType : '', 5662 employmentPhoneDial: savedEmploymentEntries[0] ? savedEmploymentEntries[0].phoneDial : '', 5663 employmentPhoneCountry: savedEmploymentEntries[0] ? savedEmploymentEntries[0].phoneCountry : '', 5664 employmentPhoneNational: savedEmploymentEntries[0] ? savedEmploymentEntries[0].phoneNational : '', 5665 employmentYears: savedEmploymentEntries[0] ? savedEmploymentEntries[0].years : '', 5666 employmentMonths: savedEmploymentEntries[0] ? savedEmploymentEntries[0].months : '', 5667 employmentPastEmployerName: savedEmploymentEntries[0] ? savedEmploymentEntries[0].pastEmployerName : '', 5668 employmentPastOccupation: savedEmploymentEntries[0] ? savedEmploymentEntries[0].pastOccupation : '', 5669 savedEmployment: savedEmploymentEntries.map((emp) => ({ 5670 status: emp.status, 5671 employerName: emp.employerName, 5672 occupation: emp.occupation, 5673 occupationalBasis: emp.occupationalBasis, 5674 occupationalType: emp.occupationalType, 5675 phoneDial: emp.phoneDial, 5676 phoneCountry: emp.phoneCountry, 5677 phoneNational: emp.phoneNational, 5678 years: emp.years, 5679 months: emp.months, 5680 pastEmployerName: emp.pastEmployerName || '', 5681 pastOccupation: emp.pastOccupation || '', 5682 })), 5683 savedAddresses: savedAddressEntries.map((a) => ({ 5684 source: a.source, 5685 line1: a.line1, 5686 line2: a.line2, 5687 townCounty: a.townCounty, 5688 postcode: a.postcode, 5689 residency: a.residency, 5690 years: a.years, 5691 months: a.months, 5692 monthsAt: a.monthsAt, 5693 selectedLine: a.selectedLine || '', 5694 lookupPostcode: a.lookupPostcode || '', 5695 })), 5696 totalAnnualIncomeBeforeTax: stripSterlingCommas(val('cars-fe-income-annual-before-tax')), 5697 typicalMonthlyTakeHomePay: stripSterlingCommas(val('cars-fe-income-monthly-take-home')), 5698 otherAnnualHouseholdIncome: stripSterlingCommas(val('cars-fe-income-annual-household-other')), 5699 monthlyMortgageRentShare: stripSterlingCommas(val('cars-fe-outgoings-monthly-mortgage-rent-share')), 5700 otherTotalMonthlyBills: stripSterlingCommas(val('cars-fe-outgoings-other-monthly-bills')), 5701 repayExistingVehicleFinanceAgreement: val('cars-fe-outgoings-repay-existing-finance'),
5702 existingVehicleLoanMonthlyPayment: stripSterlingCommas(val('cars-fe-outgoings-existing-loan-monthly-payment')), 5703 foreseeFinancialDownturn: val('cars-fe-outgoings-downturn-expected'), 5704 expectedDownturnReason: val('cars-fe-outgoings-downturn-reason'), 5705 reducedAnnualIncomeBeforeTax: stripSterlingCommas(val('cars-fe-outgoings-reduced-annual-income-before-tax')), 5706 }; 5707 }; 5708 5709 /** Mirrors server IvendiFinanceScanPayloadBuilder.ValidateJourneyForIvendiFinanceScan â keeps iVendi-bound fields complete before POST. */ 5710 const getFinanceEligibilityIvendiFieldErrors = (p) => { 5711 const errors = []; 5712 const need = (label, v) => { 5713 if (!String(v ?? '').trim()) { 5714 errors.push(`${label} is required.`); 5715 } 5716 }; 5717 5718 need('Title', p.title); 5719 need('First name', p.firstName); 5720 need('Last name', p.lastName); 5721 need('Date of birth', p.dateOfBirth); 5722 need('Email', p.email); 5723 need('Driving licence type', p.licenseType); 5724 need('Mobile number', p.phoneNational); 5725 need('Marital status', p.maritalStatus); 5726 need('Number of dependants', p.numberOfDependants); 5727 5728 const addrs = Array.isArray(p.savedAddresses) ? p.savedAddresses : []; 5729 if (addrs.length > 0) { 5730 addrs.forEach((a, i) => { 5731 const n = i + 1; 5732 need(`Address ${n} line 1`, a.line1); 5733 need(`Address ${n} postcode`, a.postcode); 5734 need(`Address ${n} town`, a.townCounty); 5735 let mo = Number(a.monthsAt); 5736 if (!mo || Number.isNaN(mo)) { 5737 const y = parseInt(String(a.years ?? ''), 10) || 0; 5738 const mm = parseInt(String(a.months ?? ''), 10) || 0; 5739 mo = y >= 15 ? 15 * 12 : y * 12 + mm; 5740 } 5741 const yearsN = parseInt(String(a.years ?? ''), 10) || 0; 5742 if (yearsN < 15 && (!mo || mo <= 0)) { 5743 errors.push(`Address ${n}: time at address is required.`); 5744 } 5745 if (String(a.source || '').toLowerCase() === 'manual' && !String(a.residency || '').trim()) { 5746 errors.push(`Address ${n} residency type is required.`); 5747 } 5748 }); 5749 } else { 5750 need('Address line 1', p.addressLine1); 5751 need('Address town', p.addressTown); 5752 need('Address postcode', p.addressPostcode); 5753 need('Address residency type', p.addressResidencyType); 5754 const y = parseInt(String(p.addressYearsAt ?? ''), 10) || 0; 5755 const mm = parseInt(String(p.addressMonthsAt ?? ''), 10) || 0; 5756 if (y < 15 && y * 12 + mm <= 0) { 5757 errors.push('Time at current address is required.'); 5758 } 5759 } 5760 5761 const empArr = Array.isArray(p.savedEmployment) ? p.savedEmployment : []; 5762 if (empArr.length > 0) { 5763 const e0 = empArr[0]; 5764 need('Employment status', e0.status); 5765 need('Employer name', e0.employerName); 5766 need('Occupation', e0.occupation); 5767 need('Occupational basis', e0.occupationalBasis); 5768 need('Occupational type', e0.occupationalType); 5769 const y = parseInt(String(e0.years ?? ''), 10) || 0; 5770 const m = parseInt(String(e0.months ?? ''), 10) || 0; 5771 if (y * 12 + m <= 0) { 5772 errors.push('Time in current employment is required.'); 5773 } else if (y === 0 && m === 0) { 5774 errors.push('Time in current employment is required.'); 5775 } 5776 } else { 5777 need('Employment status', p.employmentStatus); 5778 need('Employer name', p.employmentEmployerName); 5779 need('Occupation', p.employmentOccupation); 5780 need('Occupational basis', p.employmentOccupationalBasis); 5781 need('Occupational type', p.employmentOccupationalType); 5782 const y = parseInt(String(p.employmentYears ?? ''), 10) || 0; 5783 const m = parseInt(String(p.employmentMonths ?? ''), 10) || 0; 5784 if (y * 12 + m <= 0) { 5785 errors.push('Time in current employment is required.'); 5786 } 5787 } 5788 5789 const gross = parseSterlingInputToNumber(p.totalAnnualIncomeBeforeTax); 5790 const netMonthly = parseSterlingInputToNumber(p.typicalMonthlyTakeHomePay); 5791 const otherAnnual = parseSterlingInputToNumber(p.otherAnnualHouseholdIncome); 5792 if (Number.isNaN(gross) || gross <= 0) { 5793 errors.push('Gross annual income before tax must be greater than zero.'); 5794 } 5795 if (Number.isNaN(netMonthly) || netMonthly <= 0) { 5796 errors.push('Typical monthly take-home pay must be greater than zero.'); 5797 } 5798 if (Number.isNaN(otherAnnual) || otherAnnual < 0) { 5799 errors.push('Other annual household income cannot be negative.'); 5800 } 5801 5802 const mortgage = parseSterlingInputToNumber(p.monthlyMortgageRentShare); 5803 const bills = parseSterlingInputToNumber(p.otherTotalMonthlyBills); 5804 if (Number.isNaN(mortgage) || mortgage < 0) {
5805 errors.push('Monthly mortgage or rent share cannot be negative.'); 5806 } 5807 if (Number.isNaN(bills) || bills < 0) { 5808 errors.push('Other monthly bills cannot be negative.'); 5809 } 5810 5811 const repay = String(p.repayExistingVehicleFinanceAgreement || '').trim(); 5812 if (repay !== 'Yes' && repay !== 'No') { 5813 errors.push('Please answer whether you will repay an existing vehicle finance agreement.'); 5814 } 5815 if (repay === 'Yes') { 5816 const existingLoan = parseSterlingInputToNumber(p.existingVehicleLoanMonthlyPayment); 5817 if (Number.isNaN(existingLoan) || existingLoan < 0) { 5818 errors.push('Current monthly payment on your existing loan must be zero or more.'); 5819 } 5820 } 5821 5822 const downturn = String(p.foreseeFinancialDownturn || '').trim(); 5823 if (downturn !== 'Yes' && downturn !== 'No') { 5824 errors.push('Please answer whether you foresee a downturn in your finances.'); 5825 } 5826 if (downturn === 'Yes') { 5827 need('Expected downturn reason', p.expectedDownturnReason); 5828 const reduced = parseSterlingInputToNumber(p.reducedAnnualIncomeBeforeTax); 5829 if (Number.isNaN(reduced) || reduced < 0) { 5830 errors.push('Reduced annual income (if downturn) cannot be negative.'); 5831 } 5832 } 5833 5834 return errors; 5835 }; 5836 5837 const hideAgreementView = () => { 5838 if (agreementView) { 5839 agreementView.hidden = true; 5840 } 5841 }; 5842 5843 const formatHpCurrency = (v) => { 5844 const n = Number(v); 5845 if (v === undefined || v === null || v === '' || Number.isNaN(n)) { 5846 return null; 5847 } 5848 const isWhole = Math.abs(n - Math.round(n)) < 1e-9; 5849 return `£${n.toLocaleString('en-GB', {
5850 minimumFractionDigits: isWhole ? 0 : 2, 5851 maximumFractionDigits: isWhole ? 0 : 2, 5852 })}`; 5853 }; 5854 5855 const getActiveFinanceQuoteTypeKey = () => { 5856 const financePageQuote = getFinancePageQuoteSnapshot(); 5857 if (financePageQuote?.quoteType) { 5858 return financePageQuote.quoteType === 'PCP' ? 'PCP' : 'HP'; 5859 } 5860 const tab = document.querySelector('.tab-content-container'); 5861 const current = String(tab?.getAttribute('currentTab') || 'HP').trim().toUpperCase(); 5862 return current === 'PCP' ? 'PCP' : 'HP'; 5863 }; 5864 5865 const getFinancePageQuoteSnapshot = () => { 5866 const raw = panel.getAttribute('data-fe-finance-page-quote'); 5867 if (!raw) { 5868 return null; 5869 } 5870 try { 5871 return JSON.parse(raw); 5872 } catch { 5873 return null; 5874 } 5875 }; 5876 5877 const financeQuoteValue = (elementId) => { 5878 const el = document.getElementById(elementId); 5879 if (el) { 5880 const raw = el.getAttribute(`${getActiveFinanceQuoteTypeKey()}Value`); 5881 if (raw != null && raw !== '' && raw !== 'undefined') { 5882 return raw; 5883 } 5884 } 5885 return null; 5886 }; 5887 5888 const calculatorQuoteValue = (elementId, snapshotKey) => { 5889 // Listing/special-offer CTA binds quote onto data-fe-finance-page-quote for a different 5890 // stock than the VDP calculator â prefer that snapshot so we do not show the page car's figures. 5891 if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true') { 5892 const snapshot = getFinancePageQuoteSnapshot(); 5893 if (snapshot && snapshot[snapshotKey] != null && snapshot[snapshotKey] !== '') { 5894 return String(snapshot[snapshotKey]); 5895 } 5896 return null; 5897 } 5898 const fromVdp = financeQuoteValue(elementId); 5899 if (fromVdp != null) { 5900 return fromVdp; 5901 } 5902 const snapshot = getFinancePageQuoteSnapshot(); 5903 if (!snapshot || snapshot[snapshotKey] == null || snapshot[snapshotKey] === '') { 5904 return null; 5905 } 5906 return String(snapshot[snapshotKey]); 5907 }; 5908 5909 const financeTypeLabel = () => 5910 getActiveFinanceQuoteTypeKey() === 'PCP' ? 'Personal Contract Purchase' : 'Hire Purchase'; 5911 5912 const setAgreementText = (id, text) => { 5913 const el = document.getElementById(id); 5914 if (el && text != null && String(text).trim() !== '') { 5915 el.textContent = text; 5916 } 5917 }; 5918 5919 const setAgreementRowHidden = (rowId, hidden) => { 5920 const row = document.getElementById(rowId); 5921 if (row) { 5922 row.hidden = !!hidden; 5923 } 5924 }; 5925 5926 const formatAgreementApr = (v) => { 5927 const n = Number(v); 5928 if (v === undefined || v === null || v === '' || Number.isNaN(n)) { 5929 return null; 5930 } 5931 return `APR ${n.toFixed(1)}%`; 5932 }; 5933 5934 const formatAgreementPercent = (v) => { 5935 const n = Number(v); 5936 if (v === undefined || v === null || v === '' || Number.isNaN(n)) { 5937 return null; 5938 } 5939 return `${n.toFixed(2)}%`; 5940 }; 5941 5942 const formatAgreementMileage = (v) => { 5943 const n = Math.round(Number(v)); 5944 if (v === undefined || v === null || v === '' || Number.isNaN(n)) { 5945 return null; 5946 } 5947 return n.toLocaleString('en-GB'); 5948 }; 5949 5950 const formatAgreementExcess = (v) => { 5951 const n = Number(v); 5952 if (v === undefined || v === null || v === '' || Number.isNaN(n)) { 5953 return null; 5954 } 5955 return `${n}p`; 5956 }; 5957 5958 const formatAgreementCurrency = (v) => { 5959 const n = Number(v); 5960 if (v === undefined || v === null || v === '' || Number.isNaN(n)) { 5961 return null; 5962 } 5963 return `£${n.toLocaleString('en-GB', { 5964 minimumFractionDigits: 2, 5965 maximumFractionDigits: 2, 5966 })}`; 5967 }; 5968 5969 const syncEligibilityAgreementRepresentative = () => { 5970 const mount = document.getElementById('finance-eligibility-rep-details'); 5971 if (!mount) { 5972 return; 5973 } 5974 5975 const typeLabel = financeTypeLabel(); 5976 const quoteType = getActiveFinanceQuoteTypeKey(); 5977 const headline = 5978 document.querySelector('.finance-eligibility-agreement__vehicle-title')?.textContent?.trim() || 5979 panel?.getAttribute('data-fe-stock-headline') || 5980 'selected vehicle'; 5981 const subtitleBase = 5982 panel?.getAttribute('data-fe-stock-headline') || 5983 (panel?.getAttribute('data-fe-hide-listing') === 'true' 5984 ? headline 5985 : document.getElementById('finance-eligibility-agreement-subtitle')?.textContent?.split(' on ')[0]?.trim()) || 5986 headline; 5987 setAgreementText('finance-eligibility-agreement-subtitle', `${subtitleBase} on ${typeLabel}`); 5988 setAgreementText('finance-eligibility-agreement-rep-vehicle', `Based on a ${headline} on ${typeLabel}`); 5989 5990 const cash = calculatorQuoteValue('finance-balance-to-finance', 'cash'); 5991 const deposit = calculatorQuoteValue('finance-customer-deposit', 'deposit'); 5992 const fda = calculatorQuoteValue('finance-deposit-allowance', 'fda'); 5993 const credit = calculatorQuoteValue('finance-total-amount-credit', 'credit'); 5994 const first = calculatorQuoteValue('finance-first-payment', 'first'); 5995 const regular = calculatorQuoteValue('finance-regular-payment', 'regular'); 5996 const finalPay = calculatorQuoteValue('finance-optional-final-payment', 'finalPay'); 5997 const optFee = calculatorQuoteValue('finance-purchase-fee', 'optFee'); 5998 const term = calculatorQuoteValue('finance-duration-agreement', 'term'); 5999 const interest = calculatorQuoteValue('finance-total-charge-for-credit', 'interest'); 6000 const totalPay = calculatorQuoteValue('finance-total-amount-payable', 'totalPay'); 6001 const flat = calculatorQuoteValue('finance-interest-rate', 'flat'); 6002 const excess = calculatorQuoteValue('finance-over-mileage', 'excess'); 6003 const annualMileage = calculatorQuoteValue('finance-annual-mileage', 'annualMileage'); 6004 const financePageQuote = getFinancePageQuoteSnapshot(); 6005 let aprAttr = null; 6006 if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true' && financePageQuote?.apr != null) { 6007 aprAttr = String(financePageQuote.apr); 6008 } else { 6009 aprAttr = 6010 document.getElementById('finance-header-apr')?.getAttribute(`${quoteType}Value`) ?? 6011 document.getElementById('finance-apr')?.getAttribute(`${quoteType}Value`) ?? 6012 (financePageQuote?.apr != null ? String(financePageQuote.apr) : null); 6013 } 6014 6015 // Top card summary (unchanged design / data). 6016 if (cash != null) { 6017 const cashFormatted = formatHpCurrency(cash); 6018 if (cashFormatted) { 6019 setAgreementText('finance-eligibility-agreement-cash', cashFormatted); 6020 } 6021 } 6022 if (deposit != null) { 6023 const depositFormatted = formatHpCurrency(deposit); 6024 if (depositFormatted) { 6025 setAgreementText('finance-eligibility-agreement-deposit', depositFormatted); 6026 } 6027 } 6028 if (term != null) { 6029 const termN = Math.round(Number(term)); 6030 if (!Number.isNaN(termN)) { 6031 setAgreementText('finance-eligibility-agreement-term', `${termN} months`); 6032 } 6033 } 6034 if (annualMileage != null) { 6035 const milesN = Math.round(Number(annualMileage)); 6036 if (!Number.isNaN(milesN)) { 6037 setAgreementText( 6038 'finance-eligibility-agreement-mileage',
6039 `${milesN.toLocaleString('en-GB')} miles`, 6040 ); 6041 } 6042 } 6043 6044 // Representative Example â calculator-style label/value rows. 6045 const cashFormatted = formatAgreementCurrency(cash); 6046 if (cashFormatted) { 6047 setAgreementText('finance-eligibility-rep-cash', cashFormatted); 6048 } 6049 6050 const milesFormatted = formatAgreementMileage(annualMileage); 6051 if (milesFormatted) { 6052 setAgreementText('finance-eligibility-rep-mileage', milesFormatted); 6053 } 6054 setAgreementRowHidden('finance-eligibility-rep-row-mileage', false); 6055 6056 const depositFormatted = formatAgreementCurrency(deposit); 6057 if (depositFormatted) { 6058 setAgreementText('finance-eligibility-rep-deposit', depositFormatted); 6059 } 6060 6061 const fdaFormatted = formatAgreementCurrency(fda); 6062 const showFda = !!fdaFormatted && Number(fda) > 0; 6063 if (fdaFormatted) { 6064 setAgreementText('finance-eligibility-rep-fda', fdaFormatted); 6065 } 6066 setAgreementRowHidden('finance-eligibility-rep-row-fda', !showFda); 6067 6068 const creditFormatted = formatAgreementCurrency(credit); 6069 if (creditFormatted) { 6070 setAgreementText('finance-eligibility-rep-credit', creditFormatted); 6071 } 6072 6073 if (term != null) { 6074 const termN = Math.round(Number(term)); 6075 if (!Number.isNaN(termN)) { 6076 setAgreementText('finance-eligibility-rep-term', `${termN} months`); 6077 } 6078 } 6079 6080 const firstFormatted = formatAgreementCurrency(first); 6081 if (firstFormatted) { 6082 setAgreementText('finance-eligibility-rep-first', firstFormatted); 6083 } 6084 const regularFormatted = formatAgreementCurrency(regular); 6085 if (regularFormatted) { 6086 setAgreementText('finance-eligibility-rep-regular', regularFormatted); 6087 } 6088 6089 const finalFormatted = formatAgreementCurrency(finalPay); 6090 const showFinal = quoteType === 'PCP' && !!finalFormatted; 6091 if (finalFormatted) { 6092 setAgreementText('finance-eligibility-rep-final', finalFormatted); 6093 } 6094 setAgreementRowHidden('finance-eligibility-rep-row-final', !showFinal); 6095 6096 const feeFormatted = formatAgreementCurrency(optFee); 6097 if (feeFormatted) { 6098 setAgreementText('finance-eligibility-rep-fee', feeFormatted); 6099 } 6100 6101 const totalFormatted = formatAgreementCurrency(totalPay); 6102 if (totalFormatted) { 6103 setAgreementText('finance-eligibility-rep-total', totalFormatted); 6104 } 6105 6106 const aprFormatted = formatAgreementApr(aprAttr); 6107 if (aprFormatted) { 6108 setAgreementText('finance-eligibility-rep-apr', aprFormatted); 6109 } 6110 6111 const interestFormatted = formatAgreementCurrency(interest); 6112 if (interestFormatted) { 6113 setAgreementText('finance-eligibility-rep-interest', interestFormatted); 6114 } 6115 6116 const flatFormatted = formatAgreementPercent(flat); 6117 if (flatFormatted) { 6118 setAgreementText('finance-eligibility-rep-flat', flatFormatted); 6119 } 6120 6121 const excessFormatted = formatAgreementExcess(excess); 6122 const showExcess = quoteType === 'PCP' && !!excessFormatted; 6123 if (excessFormatted) { 6124 setAgreementText('finance-eligibility-rep-excess', excessFormatted); 6125 } 6126 setAgreementRowHidden('finance-eligibility-rep-row-excess', !showExcess); 6127 }; 6128 6129 const showAgreementView = () => { 6130 if (!agreementView || !intro) { 6131 return; 6132 } 6133 if (detailsStep) { 6134 detailsStep.hidden = true; 6135 } 6136 hideContactStep(); 6137 hideMoreStep(); 6138 hidePersonalGroupedStep(); 6139 hideAddressStep(); 6140 hideEmploymentStep(); 6141 hideIncomeOutgoingsStep(); 6142 hideOutgoingsFinalStep(); 6143 hideIncomeOutgoingsGroupedStep(); 6144 hideReviewStep(); 6145 intro.hidden = true; 6146 agreementView.hidden = false; 6147 state.screen = 'agreement'; 6148 scrollToTop(agreementView); 6149 syncEligibilityAgreementRepresentative(); 6150 syncFinanceEligibilityJourneyUrl(); 6151 }; 6152 6153 const showPersonalGroupedStep = () => { 6154 applyFormDensityVariant(); 6155 if (!intro || !personalGroupedStep || !feFormDensityComposed) { 6156 return; 6157 } 6158 hideAgreementView(); 6159 if (detailsStep) { 6160 detailsStep.hidden = true; 6161 } 6162 hideContactStep(); 6163 hideMoreStep(); 6164 hideAddressStep(); 6165 hideEmploymentStep(); 6166 hideIncomeOutgoingsStep(); 6167 hideOutgoingsFinalStep(); 6168 hideIncomeOutgoingsGroupedStep(); 6169 hideReviewStep(); 6170 intro.hidden = true; 6171 personalGroupedStep.hidden = false;
6172 state.screen = 'personalGrouped'; 6173 state.dirty = true; 6174 scrollToTop(personalGroupedStep); 6175 applyFinanceEligibilityUkDial(); 6176 validatePersonalGrouped(); 6177 syncFinanceEligibilityJourneyUrl(); 6178 }; 6179 6180 const showDetailsStep = () => { 6181 if (canRouteToGroupedSteps(personalGroupedStep)) { 6182 showPersonalGroupedStep(); 6183 return; 6184 } 6185 if (!intro || !detailsStep) { 6186 return; 6187 } 6188 hideAgreementView(); 6189 hideContactStep(); 6190 hideMoreStep(); 6191 hidePersonalGroupedStep(); 6192 hideAddressStep(); 6193 hideEmploymentStep(); 6194 hideIncomeOutgoingsStep(); 6195 hideOutgoingsFinalStep(); 6196 hideIncomeOutgoingsGroupedStep(); 6197 hideReviewStep(); 6198 intro.hidden = true; 6199 detailsStep.hidden = false; 6200 state.screen = 'details'; 6201 state.dirty = true; 6202 scrollToTop(detailsStep); 6203 validateDetails(); 6204 syncFinanceEligibilityJourneyUrl(); 6205 }; 6206 6207 const showContactStep = () => { 6208 if (!detailsStep || !contactStep) { 6209 return; 6210 } 6211 detailsStep.hidden = true; 6212 hideMoreStep(); 6213 hidePersonalGroupedStep(); 6214 hideAddressStep(); 6215 hideEmploymentStep(); 6216 hideIncomeOutgoingsStep(); 6217 hideOutgoingsFinalStep(); 6218 hideIncomeOutgoingsGroupedStep(); 6219 hideReviewStep(); 6220 contactStep.hidden = false; 6221 state.screen = 'contact'; 6222 state.dirty = true; 6223 scrollToTop(contactStep); 6224 applyFinanceEligibilityUkDial(); 6225 validateContact(); 6226 syncFinanceEligibilityJourneyUrl(); 6227 }; 6228 6229 const showMoreInformationStep = () => { 6230 if (!contactStep || !moreStep) { 6231 return; 6232 } 6233 contactStep.hidden = true; 6234 hidePersonalGroupedStep(); 6235 hideAddressStep(); 6236 hideEmploymentStep(); 6237 hideIncomeOutgoingsStep(); 6238 hideOutgoingsFinalStep(); 6239 hideIncomeOutgoingsGroupedStep(); 6240 hideReviewStep(); 6241 moreStep.hidden = false; 6242 state.screen = 'more'; 6243 state.dirty = true; 6244 scrollToTop(moreStep); 6245 validateMore(); 6246 syncFinanceEligibilityJourneyUrl(); 6247 }; 6248 6249 const showAddressStep = () => { 6250 if (!addressStep) { 6251 return; 6252 } 6253 if (!feFormDensityComposed && !moreStep) { 6254 return; 6255 } 6256 hideMoreStep(); 6257 hideContactStep(); 6258 hidePersonalGroupedStep(); 6259 hideEmploymentStep(); 6260 hideIncomeOutgoingsStep(); 6261 hideOutgoingsFinalStep(); 6262 hideIncomeOutgoingsGroupedStep(); 6263 hideReviewStep(); 6264 if (detailsStep) { 6265 detailsStep.hidden = true; 6266 } 6267 // Discard any leftover mid-edit state so Next works from saved addresses. 6268 cancelUnsavedAddressEdit(); 6269 addressStep.hidden = false; 6270 delete addressStep.dataset.carsFeAddressEntryActive; 6271 setManualAddressMode(false); 6272 state.screen = 'address'; 6273 state.dirty = true; 6274 scrollToTop(addressStep); 6275 syncAddressMonthsDisabledForFifteenPlus(); 6276 validateAddress(); 6277 runAddressLookup(); 6278 syncFinanceEligibilityJourneyUrl(); 6279 }; 6280 6281 const showMoreFromAddress = () => { 6282 if (canRouteToGroupedSteps(personalGroupedStep)) { 6283 cancelUnsavedAddressEdit(); 6284 addressStep.hidden = true; 6285 hideEmploymentStep(); 6286 hideIncomeOutgoingsStep(); 6287 hideOutgoingsFinalStep(); 6288 hideIncomeOutgoingsGroupedStep(); 6289 hideReviewStep(); 6290 showPersonalGroupedStep(); 6291 return; 6292 } 6293 if (!moreStep || !addressStep) { 6294 return; 6295 } 6296 cancelUnsavedAddressEdit(); 6297 addressStep.hidden = true; 6298 hideEmploymentStep(); 6299 hideIncomeOutgoingsStep(); 6300 hideOutgoingsFinalStep(); 6301 hideIncomeOutgoingsGroupedStep(); 6302 hideReviewStep(); 6303 moreStep.hidden = false; 6304 state.screen = 'more'; 6305 scrollToTop(moreStep); 6306 validateMore(); 6307 syncFinanceEligibilityJourneyUrl(); 6308 }; 6309
6310 const showDetailsFromContact = () => { 6311 if (!detailsStep || !contactStep) { 6312 return; 6313 } 6314 contactStep.hidden = true; 6315 hideMoreStep(); 6316 hidePersonalGroupedStep(); 6317 hideAddressStep(); 6318 hideEmploymentStep(); 6319 hideIncomeOutgoingsStep(); 6320 hideOutgoingsFinalStep(); 6321 hideIncomeOutgoingsGroupedStep(); 6322 hideReviewStep(); 6323 detailsStep.hidden = false; 6324 state.screen = 'details'; 6325 scrollToTop(detailsStep); 6326 validateDetails(); 6327 syncFinanceEligibilityJourneyUrl(); 6328 }; 6329 6330 const showContactFromMore = () => { 6331 if (!contactStep || !moreStep) { 6332 return; 6333 } 6334 moreStep.hidden = true; 6335 hidePersonalGroupedStep(); 6336 hideAddressStep(); 6337 hideEmploymentStep(); 6338 hideIncomeOutgoingsStep(); 6339 hideOutgoingsFinalStep(); 6340 hideIncomeOutgoingsGroupedStep(); 6341 hideReviewStep(); 6342 contactStep.hidden = false; 6343 state.screen = 'contact'; 6344 scrollToTop(contactStep); 6345 validateContact(); 6346 syncFinanceEligibilityJourneyUrl(); 6347 }; 6348 6349 const showIncomeOutgoingsGroupedStep = () => { 6350 applyFormDensityVariant(); 6351 if (!addressStep || !incomeOutgoingsGroupedStep || !feFormDensityComposed) { 6352 return; 6353 } 6354 hidePersonalGroupedStep(); 6355 hideMoreStep(); 6356 hideContactStep(); 6357 if (detailsStep) { 6358 detailsStep.hidden = true; 6359 } 6360 hideEmploymentStep(); 6361 hideIncomeOutgoingsStep(); 6362 hideOutgoingsFinalStep(); 6363 hideReviewStep(); 6364 addressStep.hidden = true; 6365 incomeOutgoingsGroupedStep.hidden = false; 6366 state.screen = 'incomeOutgoingsGrouped'; 6367 state.dirty = true; 6368 scrollToTop(incomeOutgoingsGroupedStep); 6369 syncEmploymentSummaryUiState(); 6370 validateIncomeOutgoingsGrouped(); 6371 syncFinanceEligibilityJourneyUrl(); 6372 }; 6373 6374 const showEmploymentStep = () => { 6375 if (canRouteToGroupedSteps(incomeOutgoingsGroupedStep)) { 6376 showIncomeOutgoingsGroupedStep(); 6377 return; 6378 } 6379 if (!addressStep || !employmentStep) { 6380 return; 6381 } 6382 hideIncomeOutgoingsStep(); 6383 hideOutgoingsFinalStep(); 6384 hideIncomeOutgoingsGroupedStep(); 6385 hideReviewStep(); 6386 hideMoreStep(); 6387 hideContactStep(); 6388 hidePersonalGroupedStep(); 6389 if (detailsStep) { 6390 detailsStep.hidden = true; 6391 } 6392 addressStep.hidden = true; 6393 employmentStep.hidden = false; 6394 state.screen = 'employment'; 6395 state.dirty = true; 6396 scrollToTop(employmentStep); 6397 syncEmploymentSummaryUiState(); 6398 validateEmployment(); 6399 syncFinanceEligibilityJourneyUrl(); 6400 }; 6401 6402 const showIncomeOutgoingsStep = () => { 6403 if (canRouteToGroupedSteps(incomeOutgoingsGroupedStep)) { 6404 showIncomeOutgoingsGroupedStep(); 6405 return; 6406 } 6407 if (!employmentStep || !incomeOutgoingsStep) { 6408 return; 6409 } 6410 hideOutgoingsFinalStep(); 6411 hideIncomeOutgoingsGroupedStep(); 6412 hideReviewStep(); 6413 employmentStep.hidden = true; 6414 incomeOutgoingsStep.hidden = false; 6415 state.screen = 'incomeOutgoings'; 6416 state.dirty = true; 6417 scrollToTop(incomeOutgoingsStep); 6418 validateIncomeOutgoings(); 6419 syncFinanceEligibilityJourneyUrl(); 6420 }; 6421 6422 const showOutgoingsFinalStep = () => { 6423 if (!incomeOutgoingsStep || !outgoingsFinalStep) { 6424 return; 6425 } 6426 hideReviewStep(); 6427 incomeOutgoingsStep.hidden = true; 6428 outgoingsFinalStep.hidden = false; 6429 state.screen = 'outgoingsFinal'; 6430 state.dirty = true; 6431 scrollToTop(outgoingsFinalStep); 6432 validateOutgoingsFinal(); 6433 syncFinanceEligibilityJourneyUrl(); 6434 }; 6435 6436 const backToIntro = () => { 6437 clearReviewEditReturnPending(); 6438 hideAgreementView(); 6439 hideContactStep(); 6440 hideMoreStep(); 6441 hidePersonalGroupedStep(); 6442 hideAddressStep(); 6443 hideEmploymentStep(); 6444 hideIncomeOutgoingsStep(); 6445 hideOutgoingsFinalStep(); 6446 hideIncomeOutgoingsGroupedStep(); 6447 hideReviewStep(); 6448 if (detailsStep) { 6449 detailsStep.hidden = true; 6450 } 6451 if (intro) { 6452 intro.hidden = false;
6453 } 6454 state.screen = 'intro'; 6455 state.dirty = false; 6456 scrollToTop(intro); 6457 syncFinanceEligibilityJourneyUrl(); 6458 }; 6459 6460 const resetJourneyForms = () => { 6461 feForgetTouchedFields(); 6462 closeFinanceEligibilityResultsModal(); 6463 eligibilityQuotesCache = []; 6464 ['cars-fe-title', 'cars-fe-first-name', 'cars-fe-last-name', 'cars-fe-dob', 'cars-fe-marital'].forEach((id) => { 6465 const el = document.getElementById(id); 6466 if (el) { 6467 el.value = ''; 6468 } 6469 }); 6470 syncDobPlaceholderState(); 6471 detailsStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 6472 setBtnNextDisabled(nextBtn, true); 6473 6474 const email = document.getElementById('cars-fe-email'); 6475 if (email) { 6476 email.value = ''; 6477 } 6478 const national = document.getElementById('cars-fe-phone-national'); 6479 if (national) { 6480 national.value = ''; 6481 } 6482 applyFinanceEligibilityUkDial(); 6483 ['cars-fe-license-type', 'cars-fe-dependants'].forEach((id) => { 6484 const el = document.getElementById(id); 6485 if (el) { 6486 el.value = ''; 6487 } 6488 }); 6489 moreStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 6490 personalGroupedStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 6491 getEmploymentUiHost()?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 6492 const employmentStatusHidden = document.getElementById('cars-fe-employment-status'); 6493 if (employmentStatusHidden) { 6494 employmentStatusHidden.value = ''; 6495 } 6496 [ 6497 'cars-fe-employment-employer-name', 6498 'cars-fe-employment-occupation', 6499 'cars-fe-employment-occupation-q', 6500 'cars-fe-employment-occupational-basis', 6501 'cars-fe-employment-occupational-type', 6502 'cars-fe-employment-years', 6503 'cars-fe-employment-months', 6504 ].forEach((id) => { 6505 const el = document.getElementById(id); 6506 if (el) { 6507 el.value = financeEligibilityFieldDefaultValue(id); 6508 } 6509 }); 6510 financeEligibilityCloseOccupationPanels(getEmploymentUiHost()); 6511 setBtnNextDisabled(contactNextBtn, true); 6512 setBtnNextDisabled(moreNextBtn, true); 6513 setBtnNextDisabled(personalGroupedNextBtn, true); 6514 setBtnNextDisabled(employmentNextBtn, true); 6515 setBtnNextDisabled(incomeNextBtn, true); 6516 setBtnNextDisabled(outgoingsSummaryBtn, true); 6517 setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, true); 6518 6519 ['cars-fe-income-annual-before-tax', 'cars-fe-income-monthly-take-home', 'cars-fe-income-annual-household-other'].forEach( 6520 (id) => { 6521 const el = document.getElementById(id); 6522 if (el) { 6523 el.value = ''; 6524 } 6525 }, 6526 ); 6527 6528 [ 6529 'cars-fe-outgoings-monthly-mortgage-rent-share', 6530 'cars-fe-outgoings-other-monthly-bills', 6531 'cars-fe-outgoings-existing-loan-monthly-payment', 6532 'cars-fe-outgoings-reduced-annual-income-before-tax', 6533 ].forEach((id) => { 6534 const el = document.getElementById(id); 6535 if (el) { 6536 el.value = ''; 6537 } 6538 }); 6539 const downturnReasonReset = document.getElementById('cars-fe-outgoings-downturn-reason'); 6540 if (downturnReasonReset) { 6541 downturnReasonReset.value = ''; 6542 } 6543 outgoingsFinalStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 6544 incomeOutgoingsGroupedStep?.querySelectorAll('.cars-fe-form-stack--outgoings-final .cars-fe-pill.is-selected').forEach((p) => 6545 p.classList.remove('is-selected'), 6546 ); 6547 const repayHidden = document.getElementById('cars-fe-outgoings-repay-existing-finance'); 6548 if (repayHidden) { 6549 repayHidden.value = ''; 6550 }
6551 const downturnHidden = document.getElementById('cars-fe-outgoings-downturn-expected'); 6552 if (downturnHidden) { 6553 downturnHidden.value = ''; 6554 } 6555 const repayFollowReset = document.getElementById('cars-fe-outgoings-repay-followup'); 6556 if (repayFollowReset) { 6557 repayFollowReset.hidden = true; 6558 } 6559 const downturnFollowReset = document.getElementById('cars-fe-outgoings-downturn-followup'); 6560 if (downturnFollowReset) { 6561 downturnFollowReset.hidden = true; 6562 } 6563 6564 clearTimeout(addressLookupTimer); 6565 addressLookupTimer = null; 6566 const lookupPc = document.getElementById('cars-fe-address-postcode'); 6567 if (lookupPc) { 6568 lookupPc.value = ''; 6569 } 6570 [ 6571 'cars-fe-address-line1', 6572 'cars-fe-address-line2', 6573 'cars-fe-address-town', 6574 'cars-fe-address-postcode-manual', 6575 'cars-fe-address-county', 6576 'cars-fe-address-residency', 6577 'cars-fe-address-years-at', 6578 'cars-fe-address-months-at', 6579 ].forEach((id) => { 6580 const el = document.getElementById(id); 6581 if (el) { 6582 el.value = financeEligibilityFieldDefaultValue(id); 6583 } 6584 }); 6585 addressStep?.classList.remove('cars-fe-address--manual-saved'); 6586 addressStep?.classList.remove('cars-fe-address--lookup-saved'); 6587 addressStep?.classList.remove('cars-fe-address--has-saved-summary'); 6588 addressStep?.classList.remove('cars-fe-address--entry-flow-visible'); 6589 if (addressStep) { 6590 delete addressStep.dataset.carsFeAddressEntryActive; 6591 } 6592 savedAddressEntries = []; 6593 editingEntryId = null; 6594 editingInsertIndex = null; 6595 lastSavedAddressCardsRenderKey = null; 6596 savedEmploymentEntries = []; 6597 employmentFormMode = 'editing'; 6598 editingEmploymentId = null; 6599 editingEmploymentIndex = null; 6600 clearReviewEditReturnPending(); 6601 employmentStep?.classList.remove('cars-fe-employment--has-saved-summary'); 6602 incomeOutgoingsGroupedStep?.classList.remove( 6603 'cars-fe-employment--has-saved-summary', 6604 'cars-fe-employment--entry-flow-visible', 6605 'cars-fe-occupation-dropdown-open', 6606 'cars-fe-duration-dropdown-open', 6607 ); 6608 const empSavedPanel = document.getElementById('cars-fe-employment-saved-panel'); 6609 if (empSavedPanel) { 6610 empSavedPanel.hidden = true; 6611 } 6612 const empAddAnother = document.getElementById('cars-fe-employment-add-another-card'); 6613 if (empAddAnother) { 6614 empAddAnother.hidden = true; 6615 } 6616 const empEntryBlock = document.getElementById('cars-fe-employment-entry-block'); 6617 if (empEntryBlock) { 6618 empEntryBlock.hidden = false; 6619 } 6620 const empMount = document.getElementById('cars-fe-employment-saved-mount'); 6621 if (empMount) { 6622 empMount.textContent = ''; 6623 } 6624 const savedSummaryReset = document.getElementById('cars-fe-address-saved-summary-panel'); 6625 if (savedSummaryReset) { 6626 savedSummaryReset.hidden = true; 6627 } 6628 const historyAlertReset = document.getElementById('cars-fe-address-history-alert'); 6629 if (historyAlertReset) { 6630 historyAlertReset.hidden = true; 6631 } 6632 const employmentHistoryAlertReset = document.getElementById('cars-fe-employment-history-alert'); 6633 if (employmentHistoryAlertReset) { 6634 employmentHistoryAlertReset.hidden = true; 6635 } 6636 const addrEntryBlockReset = document.getElementById('cars-fe-address-entry-block'); 6637 if (addrEntryBlockReset) { 6638 addrEntryBlockReset.hidden = false; 6639 } 6640 renderSavedAddressCards(); 6641 populateAddressSelect([]); 6642 setManualAddressMode(false); 6643 syncAllFinanceDurationSelectShells(); 6644 syncEmploymentFormFieldLabelsForContext(); 6645 syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status')); 6646 if (reviewConsentCb) { 6647 reviewConsentCb.checked = false;
6648 } 6649 syncReviewConsentError(false); 6650 hideReviewStep(); 6651 }; 6652 6653 document.querySelectorAll('.finance-free-check-button').forEach((btn) => { 6654 btn.addEventListener('click', (e) => { 6655 e.preventDefault(); 6656 e.stopPropagation(); 6657 const sidepanelTestActive = 6658 typeof window.isFinanceEligibilitySidepanelVariantActive === 'function' && 6659 window.isFinanceEligibilitySidepanelVariantActive(); 6660 if (sidepanelTestActive) { 6661 if (!window.bootstrap || !bootstrap.Offcanvas) { 6662 return; 6663 } 6664 bootstrap.Offcanvas.getOrCreateInstance(panel).show(); 6665 } else if (typeof window.freeFinanceCheckFromVdp === 'function') { 6666 window.freeFinanceCheckFromVdp(); 6667 } 6668 }); 6669 }); 6670 6671 document.querySelectorAll('[data-fe-open-agreement]').forEach((btn) => { 6672 btn.addEventListener('click', (e) => { 6673 e.preventDefault(); 6674 showAgreementView(); 6675 }); 6676 }); 6677 6678 document.getElementById('finance-eligibility-agreement-close-button')?.addEventListener('click', (e) => { 6679 e.preventDefault(); 6680 backToIntro(); 6681 }); 6682 6683 document.getElementById('finance-eligibility-start-button')?.addEventListener('click', (e) => { 6684 e.preventDefault(); 6685 applyFormDensityVariant(); 6686 showDetailsStep(); 6687 }); 6688 6689 document.getElementById('cars-fe-footer-back-button')?.addEventListener('click', (e) => { 6690 e.preventDefault(); 6691 clearReviewEditReturnPending(); 6692 if (state.screen === 'details') { 6693 backToIntro(); 6694 } 6695 }); 6696 6697 document.getElementById('cars-fe-personal-grouped-footer-back-button')?.addEventListener('click', (e) => { 6698 e.preventDefault(); 6699 clearReviewEditReturnPending(); 6700 if (state.screen === 'personalGrouped') { 6701 backToIntro(); 6702 } 6703 }); 6704 6705 document.getElementById('cars-fe-contact-footer-back-button')?.addEventListener('click', (e) => { 6706 e.preventDefault(); 6707 clearReviewEditReturnPending(); 6708 if (state.screen === 'contact') { 6709 showDetailsFromContact(); 6710 } 6711 }); 6712 6713 nextBtn?.addEventListener('click', (e) => { 6714 e.preventDefault(); 6715 attemptFeJourneyNext('details', detailsStep, validateDetails, () => { 6716 if (reviewEditReturnPending) { 6717 showReviewStep(); 6718 return; 6719 } 6720 showContactStep(); 6721 }); 6722 }); 6723 6724 personalGroupedNextBtn?.addEventListener('click', (e) => { 6725 e.preventDefault(); 6726 attemptPersonalGroupedNext(() => { 6727 if (reviewEditReturnPending) { 6728 showReviewStep(); 6729 return; 6730 } 6731 showAddressStep(); 6732 }); 6733 }); 6734 6735 contactNextBtn?.addEventListener('click', (e) => { 6736 e.preventDefault(); 6737 attemptFeJourneyNext('contact', contactStep, validateContact, () => { 6738 if (reviewEditReturnPending) { 6739 showReviewStep(); 6740 return; 6741 } 6742 showMoreInformationStep(); 6743 }); 6744 }); 6745 6746 document.getElementById('cars-fe-more-footer-back-button')?.addEventListener('click', (e) => { 6747 e.preventDefault(); 6748 clearReviewEditReturnPending(); 6749 if (state.screen === 'more') { 6750 showContactFromMore(); 6751 } 6752 }); 6753 6754 moreNextBtn?.addEventListener('click', (e) => { 6755 e.preventDefault(); 6756 attemptFeJourneyNext('more', moreStep, validateMore, () => { 6757 if (reviewEditReturnPending) { 6758 showReviewStep(); 6759 return; 6760 } 6761 showAddressStep(); 6762 }); 6763 }); 6764 6765 document.getElementById('cars-fe-address-footer-back-button')?.addEventListener('click', (e) => { 6766 e.preventDefault(); 6767 clearReviewEditReturnPending(); 6768 if (state.screen === 'address') { 6769 showMoreFromAddress(); 6770 } 6771 }); 6772 6773 document.getElementById('cars-fe-address-manual-toggle')?.addEventListener('click', (e) => { 6774 e.preventDefault(); 6775 if (addressStep?.classList.contains('cars-fe-address--manual')) { 6776 return; 6777 } 6778 if (addressStep) { 6779 addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_MANUA
6779L_TOGGLE; 6780 } 6781 setManualAddressMode(true); 6782 }); 6783 6784 document.getElementById('cars-fe-address-manual-save-button')?.addEventListener('click', (e) => { 6785 e.preventDefault(); 6786 if (!validateManualFormForSave()) { 6787 requestFeStepFieldHighlights('address'); 6788 applyAddressFieldHighlights(); 6789 financeEligibilityScrollToFirstInvalid(addressStep); 6790 return; 6791 } 6792 const entry = 6793 typeof editingEntryId === 'string' && editingEntryId.length > 0 6794 ? buildManualEntryFromForm(editingEntryId) 6795 : buildManualEntryFromForm(); 6796 if (editingInsertIndex != null && typeof editingEntryId === 'string' && editingEntryId.length > 0) { 6797 savedAddressEntries[editingInsertIndex] = entry; 6798 editingInsertIndex = null; 6799 editingEntryId = null; 6800 } else { 6801 savedAddressEntries.push(entry); 6802 } 6803 clearManualAddressFormFields(); 6804 placeAddressManualWrapHome(); 6805 addressStep?.classList.remove('cars-fe-address--manual', 'cars-fe-address--editing-saved'); 6806 if (addressStep) { 6807 delete addressStep.dataset.carsFeManualWrapIntent; 6808 delete addressStep.dataset.carsFeAddressEntryActive; 6809 } 6810 const addressOkAfterSave = validateAddress(); 6811 if (reviewEditReturnPending && addressOkAfterSave) { 6812 showReviewStep(); 6813 } 6814 }); 6815 6816 document.getElementById('cars-fe-address-manual-delete-button')?.addEventListener('click', (e) => { 6817 e.preventDefault(); 6818 if (editingInsertIndex == null || editingEntryId == null) { 6819 return; 6820 } 6821 const ix = savedAddressEntries.findIndex((x) => x.id === editingEntryId); 6822 if (ix >= 0) { 6823 savedAddressEntries.splice(ix, 1); 6824 } else if ( 6825 typeof editingInsertIndex === 'number' && 6826 editingInsertIndex >= 0 && 6827 editingInsertIndex < savedAddressEntries.length 6828 ) { 6829 savedAddressEntries.splice(editingInsertIndex, 1); 6830 } 6831 editingEntryId = null; 6832 editingInsertIndex = null; 6833 clearManualAddressFormFields(); 6834 placeAddressManualWrapHome(); 6835 addressStep?.classList.remove('cars-fe-address--manual', 'cars-fe-address--editing-saved'); 6836 if (addressStep) { 6837 delete addressStep.dataset.carsFeManualWrapIntent; 6838 delete addressStep.dataset.carsFeAddressEntryActive; 6839 } 6840 const deleteBtn = document.getElementById('cars-fe-address-manual-delete-button'); 6841 if (deleteBtn) { 6842 deleteBtn.hidden = true; 6843 } 6844 validateAddress(); 6845 if (reviewEditReturnPending && savedAddressEntries.length > 0 && validateAddress()) { 6846 showReviewStep(); 6847 } 6848 }); 6849 6850 document.getElementById('cars-fe-address-saved-cards')?.addEventListener('click', (e) => { 6851 const btn = e.target.closest('[data-cars-fe-entry-edit]'); 6852 if (!btn || !addressStep) { 6853 return; 6854 } 6855 e.preventDefault(); 6856 const id = btn.getAttribute('data-cars-fe-entry-edit'); 6857 if (!id) { 6858 return; 6859 } 6860 const ix = savedAddressEntries.findIndex((x) => x.id === id); 6861 if (ix < 0) { 6862 return; 6863 } 6864 const entry = savedAddressEntries[ix]; 6865 if (entry.source === 'lookup') { 6866 savedAddressEntries.splice(ix, 1); 6867 const sel = document.getElementById('cars-fe-address-select'); 6868 if (sel) { 6869 sel.value = ''; 6870 } 6871 editingEntryId = null; 6872 editingInsertIndex = null; 6873 validateAddress(); 6874 return; 6875 } 6876 const savedCountBeforeEdit = savedAddressEntries.length; 6877 editingInsertIndex = ix; 6878 editingEntryId = entry.id; 6879 applyManualEntryToForm(entry); 6880 addressStep.dataset.carsFeAddressEntryActive = 'true'; 6881 addressStep.dataset.carsFeManualWrapIntent = 6882 savedCountBeforeEdit >
6882 1 ? MANUAL_WRAP_INTENT_ADD_ANOTHER : MANUAL_WRAP_INTENT_MANUAL_TOGGLE; 6883 addressStep.classList.add('cars-fe-address--manual'); 6884 validateAddress(); 6885 const editSlot = document.getElementById('cars-fe-address-edit-slot'); 6886 const manualWrap = document.getElementById('cars-fe-address-manual-wrap'); 6887 const scrollTarget = editSlot || manualWrap; 6888 if (scrollTarget) { 6889 financeEligibilityScheduleFieldScrollIntoView(scrollTarget, { behavior: 'smooth', topPaddingPx: 16 }); 6890 } else { 6891 scrollToTop(addressStep); 6892 } 6893 }); 6894 6895 document.getElementById('cars-fe-address-add-another-button')?.addEventListener('click', (e) => { 6896 e.preventDefault(); 6897 if (savedAddressEntries.length < 1 || !addressStep) { 6898 return; 6899 } 6900 addressStep.dataset.carsFeAddressEntryActive = 'true'; 6901 addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_ADD_ANOTHER; 6902 editingEntryId = null; 6903 editingInsertIndex = null; 6904 clearManualAddressFormFields(); 6905 const lookupPc = document.getElementById('cars-fe-address-postcode'); 6906 if (lookupPc) { 6907 lookupPc.value = ''; 6908 } 6909 populateAddressSelect([]); 6910 setManualAddressMode(false); 6911 validateAddress(); 6912 scrollToTop(addressStep); 6913 }); 6914 6915 /** Postcoder typeahead â replaces the legacy postcode-then-select dropdown. On select, the address 6916 * drops straight into the manual-edit fields so the user only has to add residency + time. */ 6917 (() => { 6918 if (typeof PostcoderAutocomplete !== 'function') { 6919 return; 6920 } 6921 const input = document.getElementById('cars-fe-address-postcode'); 6922 const list = document.getElementById('cars-fe-address-suggestions'); 6923 if (!input || !list) { 6924 return; 6925 } 6926 const apiKey = 6927 (panel.dataset && panel.dataset.fePostcoderApikey) || 6928 (typeof window !== 'undefined' ? window.POSTCODER_API_KEY : '') || 6929 ''; 6930 if (!apiKey) { 6931 return; 6932 } 6933 6934 const syncAddressSuggestionsOpenState = () => { 6935 if (!addressStep) { 6936 return; 6937 } 6938 const open = list.childElementCount > 0; 6939 const wasOpen = addressStep.classList.contains('cars-fe-address-suggestions-open'); 6940 addressStep.classList.toggle('cars-fe-address-suggestions-open', open); 6941 if (open && !wasOpen) { 6942 window.requestAnimationFrame(() => { 6943 financeEligibilityScrollFieldIntoView(input, { 6944 listPanelEl: list, 6945 topPaddingPx: 8, 6946 gapPx: 16, 6947 behavior: 'auto', 6948 reserveBelowPx: Math.min(280, Math.round(window.innerHeight * 0.45) || 280), 6949 }); 6950 }); 6951 } 6952 if (!open && wasOpen) { 6953 const stack = 6954 addressStep.querySelector('.cars-fe-form-stack--address') || 6955 addressStep.querySelector('.cars-fe-form-stack'); 6956 if (stack) { 6957 delete stack.dataset.feScrollPadExtra; 6958 stack.style.removeProperty('padding-bottom'); 6959 } 6960 } 6961 }; 6962 6963 new PostcoderAutocomplete({ 6964 apikey: apiKey, 6965 countrycode: 'UK', 6966 searchterm: '#cars-fe-address-postcode', 6967 suggestions: '#cars-fe-address-suggestions', 6968 singlesummary: true, 6969 addressline1: '#cars-fe-address-line1', 6970 addressline2: '#cars-fe-address-line2', 6971 posttown: '#cars-fe-address-town', 6972 county: '#cars-fe-address-county', 6973 postcode: '#cars-fe-address-postcode-manual', 6974 onAddressSelected: () => { 6975 if (!addressStep) { 6976 return; 6977 } 6978 if (!addressStep.dataset.carsFeManualWrapIntent) { 6979 addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_MANUA
6979L_TOGGLE; 6980 } 6981 setManualAddressMode(true); 6982 syncAddressSuggestionsOpenState(); 6983 const residency = document.getElementById('cars-fe-address-residency'); 6984 if (residency && typeof residency.focus === 'function') { 6985 try { 6986 residency.focus({ preventScroll: true }); 6987 } catch (err) { 6988 residency.focus(); 6989 } 6990 } 6991 validateAddress(); 6992 }, 6993 }); 6994 6995 if (typeof MutationObserver === 'function') { 6996 const suggestionsObserver = new MutationObserver(() => { 6997 syncAddressSuggestionsOpenState(); 6998 }); 6999 suggestionsObserver.observe(list, { childList: true }); 7000 } 7001 })(); 7002 7003 addressNextBtn?.addEventListener('click', (e) => { 7004 e.preventDefault(); 7005 attemptFeJourneyNext('address', addressStep, validateAddress, () => { 7006 if (reviewEditReturnPending) { 7007 showReviewStep(); 7008 return; 7009 } 7010 showEmploymentStep(); 7011 }); 7012 }); 7013 7014 document.getElementById('cars-fe-employment-footer-back-button')?.addEventListener('click', (e) => { 7015 e.preventDefault(); 7016 clearReviewEditReturnPending(); 7017 if (state.screen === 'employment') { 7018 showAddressStep(); 7019 } 7020 }); 7021 7022 document.getElementById('cars-fe-income-outgoings-grouped-footer-back-button')?.addEventListener('click', (e) => { 7023 e.preventDefault(); 7024 clearReviewEditReturnPending(); 7025 if (state.screen === 'incomeOutgoingsGrouped') { 7026 showAddressStep(); 7027 } 7028 }); 7029 7030 employmentNextBtn?.addEventListener('click', (e) => { 7031 e.preventDefault(); 7032 attemptFeJourneyNext('employment', employmentStep, validateEmployment, () => { 7033 if (reviewEditReturnPending) { 7034 showReviewStep(); 7035 return; 7036 } 7037 showIncomeOutgoingsStep(); 7038 }); 7039 }); 7040 7041 document.getElementById('cars-fe-income-footer-back-button')?.addEventListener('click', (e) => { 7042 e.preventDefault(); 7043 clearReviewEditReturnPending(); 7044 if (state.screen === 'incomeOutgoings' && employmentStep && incomeOutgoingsStep) { 7045 hideOutgoingsFinalStep(); 7046 hideReviewStep(); 7047 incomeOutgoingsStep.hidden = true; 7048 employmentStep.hidden = false; 7049 state.screen = 'employment'; 7050 scrollToTop(employmentStep); 7051 syncEmploymentSummaryUiState(); 7052 validateEmployment(); 7053 syncFinanceEligibilityJourneyUrl(); 7054 } 7055 }); 7056 7057 document.getElementById('cars-fe-outgoings-final-footer-back-button')?.addEventListener('click', (e) => { 7058 e.preventDefault(); 7059 clearReviewEditReturnPending(); 7060 if (state.screen === 'outgoingsFinal' && incomeOutgoingsStep && outgoingsFinalStep) { 7061 outgoingsFinalStep.hidden = true; 7062 incomeOutgoingsStep.hidden = false; 7063 state.screen = 'incomeOutgoings'; 7064 scrollToTop(incomeOutgoingsStep); 7065 validateIncomeOutgoings(); 7066 syncFinanceEligibilityJourneyUrl(); 7067 } 7068 }); 7069 7070 incomeNextBtn?.addEventListener('click', (e) => { 7071 e.preventDefault(); 7072 attemptFeJourneyNext('income', incomeOutgoingsStep, validateIncomeOutgoings, () => { 7073 showOutgoingsFinalStep(); 7074 }); 7075 }); 7076 7077 outgoingsSummaryBtn?.addEventListener('click', (e) => { 7078 e.preventDefault(); 7079 attemptFeJourneyNext('outgoings', outgoingsFinalStep, validateOutgoingsFinal, () => { 7080 showReviewStep(); 7081 }); 7082 }); 7083 7084 incomeOutgoingsGroupedSummaryBtn?.addEventListener('click', (e) => { 7085 e.preventDefault(); 7086 // Both routes end on the review screen, whether this was a review edit or the first pass. 7087 attemptIncomeOutgoingsGroupedNext(() => { 7088 showReviewStep(); 7089 }); 7090 }); 7091 7092 // Density now rides on the journey flag (test-c), so the journey-variant listener re-applies it. 7093 applyFormDensityVariant(); 7094 7095 const closeFinanceEligibilityResultsGlossary = () => { 7096 const glossaryLay = document.getElementById('cars-fe-results-glossary'); 7097 if (!glossaryLay || glossaryLay.hidden) { 7098 return; 7099 } 7100 glossaryLay.hidden = true; 7101 glossaryLay.setAttribute('aria-hidden', 'true'); 7102 document.getElementById('cars-fe-results-glossary-open')?.setAttribute('aria-expanded', 'false'); 7103 syncFinanceEligibilityJourneyUrl(); 7104 }; 7105 7106 const openFinanceEligibilityResultsGlossary = () => { 7107 const glossaryLay = document.getElementById('cars-fe-results-glossary'); 7108 if (!glossaryLay || !resultsModal || resultsModal.hidden) { 7109 return; 7110 } 7111 glossaryLay.hidden = false;
7112 glossaryLay.setAttribute('aria-hidden', 'false'); 7113 document.getElementById('cars-fe-results-glossary-open')?.setAttribute('aria-expanded', 'true'); 7114 document.getElementById('cars-fe-results-glossary-close')?.focus?.(); 7115 syncFinanceEligibilityJourneyUrl(); 7116 }; 7117 7118 const closeFinanceEligibilityResultsModal = () => { 7119 if (!resultsModal) { 7120 return; 7121 } 7122 setFinanceResultsModalLoading(false); 7123 closeFinanceEligibilityResultsGlossary(); 7124 const lenderLay = document.getElementById('cars-fe-results-lender-detail'); 7125 if (lenderLay && lenderLay.hidden === false) { 7126 lenderLay.hidden = true; 7127 lenderLay.setAttribute('aria-hidden', 'true'); 7128 } 7129 resultsModal.hidden = true; 7130 resultsModal.setAttribute('aria-hidden', 'true'); 7131 syncFinanceEligibilityJourneyUrl(); 7132 }; 7133 7134 /** Dismisses the whole finance offcanvas, leaving the customer on the VDP behind it. */ 7135 const closeFinanceEligibilityPanel = () => { 7136 if (!panel || !window.bootstrap || !bootstrap.Offcanvas) { 7137 closeFinanceEligibilityResultsModal(); 7138 return; 7139 } 7140 bootstrap.Offcanvas.getOrCreateInstance(panel).hide(); 7141 }; 7142 7143 const setFinanceResultsModalLoading = (on) => { 7144 const layer = document.getElementById('cars-fe-results-modal-loading'); 7145 const scroll = resultsModal?.querySelector('.cars-fe-results-modal__scroll'); 7146 const foot = resultsModal?.querySelector('.cars-fe-results-modal__footer'); 7147 const vehicleStrip = resultsModal?.querySelector('.cars-fe-results-modal__vehicle'); 7148 if (!layer || !scroll || !foot) { 7149 return; 7150 } 7151 layer.hidden = !on; 7152 layer.setAttribute('aria-hidden', on ? 'false' : 'true'); 7153 scroll.hidden = on; 7154 foot.hidden = on; 7155 if (vehicleStrip) { 7156 vehicleStrip.hidden = on; 7157 } 7158 if (on) { 7159 resultsModal.setAttribute('aria-busy', 'true'); 7160 setFinanceScanLoadingRetryVisible(false); 7161 } else { 7162 resultsModal.removeAttribute('aria-busy'); 7163 feScanPollGeneration += 1; 7164 feScanPollRetryContext = null; 7165 setFinanceScanLoadingRetryVisible(false); 7166 } 7167 syncFinanceEligibilityJourneyUrl(); 7168 }; 7169 7170 const syncResultsVehicleStripFromPanel = () => { 7171 if (!panel || !resultsModal) { 7172 return; 7173 } 7174 const headline = panel.getAttribute('data-fe-stock-headline'); 7175 const reg = panel.getAttribute('data-fe-stock-registration'); 7176 const loc = panel.getAttribute('data-fe-stock-location'); 7177 if (headline) { 7178 resultsModal 7179 .querySelectorAll( 7180 '.cars-fe-results-modal__vehicle .cars-fe-listing__title, .cars-fe-results-modal__loading-vehicle .cars-fe-listing__title', 7181 ) 7182 .forEach((titleEl) => { 7183 titleEl.textContent = headline; 7184 }); 7185 } 7186 ['cars-fe-results-modal-reg', 'cars-fe-results-modal-loading-reg'].forEach((id) => { 7187 const regEl = document.getElementById(id); 7188 if (regEl && reg) { 7189 regEl.textContent = reg; 7190 } 7191 }); 7192 const locEl = document.getElementById('cars-fe-results-modal-location'); 7193 if (locEl && loc) { 7194 locEl.textContent = loc; 7195 } 7196 }; 7197 7198 /** Keep journey listing chrome + agreement title in sync when binding a VLP car onto a shared panel. */ 7199 const syncFinanceEligibilityListingFromPanel = () => { 7200 if (!panel) { 7201 return; 7202 } 7203 const headline = panel.getAttribute('data-fe-stock-headline'); 7204 const reg = panel.getAttribute('data-fe-stock-registration'); 7205 if (headline) { 7206 panel.querySelectorAll('.cars-fe-listing__title').forEach((titleEl) => { 7207 titleEl.textContent = headline; 7208 }); 7209 const agreementTitle = panel.querySelector('.finance-eligibility-agreement__vehicle-title'); 7210 if (agreementTitle) { 7211 agreementTitle.textContent = headline; 7212 } 7213 } 7214 if (reg) { 7215 panel.querySelectorAll('.cars-fe-listing__reg').forEach((regEl) => { 7216 regEl.textContent = reg; 7217 }); 7218 } 7219 syncFinanceEligibilityIntroVehicleCardFromPanel(); 7220 syncResultsVehicleStripFromPanel(); 7221 }; 7222 7223 /** 7224 * The vehicle-led intro card (test-b and test-c) is rendered empty in the shared drawer (VLP cards 7225 * have no page car), so fill its photo and stats from the attributes the opening CTA put on the 7226 * panel. VDP renders the card server-side from its own car, so only the listing-CTA path fills it. 7227 */ 7228 const syncFinanceEligibilityIntroVehicleCardFromPanel = () => { 7229 if (!panel || panel.getAttribute('data-fe-bound-listing-cta') !== 'true') { 7230 return; 7231 } 7232 const card = panel.querySelector('.finance-eligibility-intro__vehicle-led .finance-eligibility-intro__vehicle'); 7233 if (!card) { 7234 return; 7235 } 7236 7237 const image = panel.getAttribute('data-fe-stock-image'); 7238 const imageEl = card.querySelector('.finance-eligibility-intro__vehicle-image img'); 7239 if (image && imageEl) { 7240 const alt = panel.getAttribute('data-fe-stock-headline') || ''; 7241 // The card image is lozad-lazy, so data-src has to move too or the observer puts the 7242 // server-rendered placeholder back. data-loaded stops it re-running for the next car. 7243 imageEl.setAttribute('data-src', image); 7244 imageEl.setAttribute('data-alt', alt); 7245 imageEl.setAttribute('data-loaded', 'true'); 7246 imageEl.src = image; 7247 imageEl.alt = alt; 7248 } 7249 7250 const setStat = (statKey, value) => { 7251 if (value == null || value === '') { 7252 return; 7253 } 7254 const statEl = card.querySelector(`[data-fe-vehicle-stat="${statKey}"]`); 7255 if (statEl) { 7256 statEl.textContent = value; 7257 } 7258 }; 7259 7260 const price = Number(panel.getAttribute('data-fe-stock-price')); 7261 if (Number.isFinite(price) && price > 0) { 7262 setStat('price', `£${new Intl.NumberFormat('en-GB').format(Math.round(price))}`); 7263 } 7264 7265 const mileage = Number(panel.getAttribute('data-fe-stock-mileage')); 7266 if (Number.isFinite(mileage) && mileage >= 0) { 7267 setStat('mileage', new Intl.NumberFormat('en-GB').format(Math.round(mileage))); 7268 } 7269 7270 setStat('term', panel.getAttribute('data-fe-finance-term')); 7271 }; 7272 7273 /** 7274 * Apply stock/get-finance-quote payload onto the eligibility offcanvas for the journey. 7275 */ 7276 const applyFinanceQuoteResponseToEligibilityPanel = (data, options = {}) => { 7277 if (!panel || !data) { 7278 return null; 7279 } 7280 7281 const annualMileage = options.annualMileage != null ? Number(options.annualMileage) : 10000; 7282 const preferType = 7283 options.financeQuoteType === 0 || options.financeQuoteType === '0' || options.financeQuoteType === 'PCP' 7284 ? 'pcp' 7285 : options.financeQuoteType === 1 || options.financeQuoteType === '1' || options.financeQuoteType === 'HP' 7286 ? 'hp' 7287 : null; 7288 7289 const quotes = Array.isArray(data) ? data : data && Array.isArray(data.quotes) ? data.quotes : []; 7290 let eligibilityQuoteIds = []; 7291 if (data && Array.isArray(data.eligibilityQuoteIds)) { 7292 eligibilityQuoteIds = data.eligibilityQuoteIds 7293 .filter((id) => id && String(id).trim()) 7294 .map((id) => String(id).trim()); 7295 } else { 7296 eligibilityQuoteIds = quotes 7297 .map((q) => q && q.QuoteUID) 7298 .filter((id) => id && String(id).trim()) 7299 .map((id) => String(id).trim()); 7300 } 7301 if (eligibilityQuoteIds.length > 0) { 7302 panel.setAttribute('data-fe-finance-eligibility-quote-ids', JSON.stringify(eligibilityQuoteIds)); 7303 } else { 7304 panel.removeAttribute('data-fe-finance-eligibility-quote-ids'); 7305 } 7306 7307 if (data && data.connectRetailerId) { 7308 panel.setAttribute('data-fe-ivendi-retailer-id', String(data.connectRetailerId).trim()); 7309 } 7310 if (data && data.retailerId) { 7311 panel.setAttribute('data-fe-quoteware-dealership-id', String(data.retailerId).trim()); 7312 } 7313 if (data && data.siteId != null) { 7314 panel.setAttribute('data-fe-site-id', String(data.siteId)); 7315 } 7316 if (data && data.stockId != null) { 7317 panel.setAttribute('data-fe-stock-id', String(data.stockId)); 7318 } 7319 7320 const quote = 7321 (preferType && quotes.find((q) => q && String(q.Type || '').toLowerCase() === preferType)) || 7322 quotes.find((q) => q); 7323 if (!quote) { 7324 return null; 7325 } 7326 7327 const quoteType = String(quote.Type || 'PCP').trim().toUpperCase() === 'HP' ? 'HP' : 'PCP'; 7328 const termN = Math.round(Number(quote.Term)); 7329 let numRegular = 7330 quote.NumberOfRegularPayments != null ? Math.round(Number(quote.NumberOfRegularPayments)) : NaN; 7331 if (Number.isNaN(numRegular) && !Number.isNaN(termN) && termN >= 3) { 7332 numRegular = termN - 2; 7333 } 7334 const cash = Number(quote.TotalCashPrice); 7335 const dep = Number(quote.TotalDeposit); 7336 const credit = 7337 quote.Advance != null && quote.Advance !== '' 7338 ? Number(quote.Advance) 7339 : !Number.isNaN(cash) && !Number.isNaN(dep) 7340 ? cash - dep 7341 : null; 7342 7343 panel.setAttribute( 7344 'data-fe-finance-page-quote', 7345 JSON.stringify({ 7346 funderCode: quote.FunderCode || '', 7347 productName: quote.ProductName || '', 7348 quoteType, 7349 cash: quote.TotalCashPrice, 7350 deposit: quote.TotalDeposit, 7351 fda: quote.DepositAllowance, 7352 credit, 7353 first: quote.FirstPayment, 7354 regular: quote.RegularPayment, 7355 finalPay: quote.FinalPayment, 7356 optFee: quote.OptionToPurchaseFee, 7357 term: quote.Term, 7358 annualMileage, 7359 interest: quote.TotalCharges, 7360 totalPay: quote.TotalPayable, 7361 flat: quote.FlatRate, 7362 apr: quote.APR, 7363 excess: quote.ChargePerOverDistance, 7364 numRegular: Number.isNaN(numRegular) ? null : numRegular, 7365 }), 7366 ); 7367 if (!Number.isNaN(termN) && termN > 0) {
7368 panel.setAttribute('data-fe-finance-term', String(termN)); 7369 } 7370 return quote; 7371 }; 7372 7373 /** 7374 * VLP card CTA: bind stock + stock/get-finance-quote result, then open the eligibility offcanvas. 7375 * Prefer options.quoteResponse from the CTA click (already fetched); otherwise fetch here. 7376 * Closing the panel leaves the user on the listing page (hash cleared on hidden). 7377 */ 7378 window.openVlpFinanceEligibilityForCar = async (car, options = {}) => { 7379 if (!panel || !car || !window.bootstrap || !bootstrap.Offcanvas) { 7380 throw new Error('Finance eligibility panel is not ready'); 7381 } 7382 7383 const termMonths = parseInt(options.termMonths, 10) || car.LowestPaymentTerm || 48; 7384 let deposit = options.deposit != null ? Number(options.deposit) : NaN; 7385 if (!Number.isFinite(deposit) || deposit <= 0) { 7386 deposit = Math.round((Number(car.AskingPrice) * 0.1) / 1000) * 1000; 7387 } 7388 const annualMileage = 7389 options.annualMileage != null && Number.isFinite(Number(options.annualMileage)) 7390 ? Number(options.annualMileage) 7391 : 10000; 7392 7393 // Prefer get-finance-quote snapshot over VDP calculator DOM (page car may differ). 7394 panel.setAttribute('data-fe-bound-listing-cta', 'true'); 7395 panel.removeAttribute('data-fe-hide-listing'); 7396 panel.setAttribute('data-fe-stock-id', String(car.StockNumber)); 7397 if (car.Registration) { 7398 panel.setAttribute('data-fe-stock-registration', String(car.Registration).trim()); 7399 } 7400 if (car.HeadlineDescription) { 7401 panel.setAttribute('data-fe-stock-headline', String(car.HeadlineDescription).trim()); 7402 } 7403 if (car.DisplayLocation) { 7404 panel.setAttribute('data-fe-stock-location', String(car.DisplayLocation).trim()); 7405 } 7406 if (car.SiteID != null) { 7407 panel.setAttribute('data-fe-site-id', String(car.SiteID)); 7408 } 7409 // Feeds the test-B intro vehicle card, which the shared drawer renders empty. 7410 const cardImage = Array.isArray(car.SliderImages) && car.SliderImages.length > 0 7411 ? car.SliderImages[0].ClearImage 7412 : null; 7413 if (cardImage) { 7414 panel.setAttribute('data-fe-stock-image', String(cardImage)); 7415 } 7416 if (Number.isFinite(Number(car.AskingPrice))) { 7417 panel.setAttribute('data-fe-stock-price', String(Number(car.AskingPrice))); 7418 } 7419 if (Number.isFinite(Number(car.Mileage))) { 7420 panel.setAttribute('data-fe-stock-mileage', String(Number(car.Mileage))); 7421 } 7422 panel.setAttribute('data-fe-finance-term', String(termMonths)); 7423 syncFinanceEligibilityListingFromPanel(); 7424 7425 let quoteResponse = options.quoteResponse || null; 7426 if (!quoteResponse) { 7427 if (typeof API === 'undefined' || !API.Stock || typeof API.Stock.GetFinanceQuote !== 'function') { 7428 throw new Error('stock/get-finance-quote API unavailable'); 7429 } 7430 quoteResponse = await API.Stock.GetFinanceQuote( 7431 car.StockNumber, 7432 termMonths, 7433 annualMileage, 7434 deposit, 7435 ); 7436 } 7437 7438 const appliedQuote = applyFinanceQuoteResponseToEligibilityPanel(quoteResponse, { 7439 annualMileage, 7440 financeQuoteType: options.financeQuoteType, 7441 effectiveFinanceFilter: options.effectiveFinanceFilter, 7442 }); 7443 if (!appliedQuote) { 7444 throw new Error('No finance quote returned for this vehicle'); 7445 } 7446 7447 syncEligibilityAgreementRepresentative(); 7448 syncReviewConsentLenderName(); 7449 bootstrap.Offcanvas.getOrCreateInstance(panel).show(); 7450 return quoteResponse; 7451 }; 7452 7453 const formatFeMoney = (n) => { 7454 const x = Number(n); 7455 if (!Number.isFinite(x)) { 7456 return 'â'; 7457 } 7458 return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(x); 7459 }; 7460 7461 const formatFeApr = (n) => { 7462 const x = Number(n); 7463 if (!Number.isFinite(x)) { 7464 return 'â'; 7465 } 7466 const s = x.toFixed(1); 7467 return `${s}%`; 7468 }; 7469 7470 const firstNonBlank = (...vals) => { 7471 for (let i = 0; i < vals.length; i += 1) { 7472 const v = vals[i]; 7473 if (v == null) { 7474 continue; 7475 } 7476 if (typeof v === 'string') { 7477 if (v.trim() !== '') { 7478 return v; 7479 } 7480 continue; 7481 } 7482 return v; 7483 } 7484 return null; 7485 }; 7486 7487 const toFiniteNumberOrNull = (v) => {
7488 const n = Number(v); 7489 return Number.isFinite(n) ? n : null; 7490 }; 7491 7492 const pickPaymentSchedules = (figures) => { 7493 if (Array.isArray(figures.PaymentSchedules)) { 7494 return figures.PaymentSchedules; 7495 } 7496 if (Array.isArray(figures.paymentSchedules)) { 7497 return figures.paymentSchedules; 7498 } 7499 return []; 7500 }; 7501 7502 const pickRegularSchedule = (schedules) => { 7503 const byPeriod = schedules.find((s) => Number(s?.Period) === 1); 7504 if (byPeriod) { 7505 return byPeriod; 7506 } 7507 return schedules.find((s) => Number(s?.period) === 1) || null; 7508 }; 7509 7510 const resolveRegularPaymentFromFigures = (figures, regularSchedule) => { 7511 const fromFigures = 7512 toFiniteNumberOrNull(figures.RegularPayment) ?? 7513 toFiniteNumberOrNull(figures.regularPayment); 7514 if (fromFigures != null) { 7515 return fromFigures; 7516 } 7517 const fromSchedule = 7518 toFiniteNumberOrNull(regularSchedule?.Amount) ?? 7519 toFiniteNumberOrNull(regularSchedule?.amount); 7520 if (fromSchedule != null) { 7521 return fromSchedule; 7522 } 7523 return ( 7524 toFiniteNumberOrNull(figures.FirstPayment) ?? 7525 toFiniteNumberOrNull(figures.firstPayment) 7526 ); 7527 }; 7528 7529 const resolveFinanceTypeFromResult = (quoteRoot, productQuote, balloon) => { 7530 const declaredType = firstNonBlank( 7531 quoteRoot.Type, 7532 quoteRoot.type, 7533 productQuote.Type, 7534 productQuote.type, 7535 ); 7536 if (declaredType != null && String(declaredType).trim() !== '') { 7537 return String(declaredType).trim(); 7538 } 7539 if (balloon != null && balloon > 0) { 7540 return 'PCP'; 7541 } 7542 return 'HP'; 7543 }; 7544 7545 const resolveEligibilityOfferedFromResultRow = (raw) => 7546 firstNonBlank( 7547 raw.EligibilityOffered, 7548 raw.eligibilityOffered, 7549 raw?.LenderData?.Eligibility, 7550 raw?.lenderData?.eligibility, 7551 ); 7552 7553 /** 7554 * Normalizes one Data.Results[] row for BMW UX cards/detail. 7555 * Finance figures: OfferedQuote.ProductQuote.Figures (+ Funder). 7556 * Eligibility badge: EligibilityOffered (LenderData.Eligibility fallback only). 7557 */ 7558 const normalizeFinanceEligibilityQuote = (raw) => { 7559 if (!raw || typeof raw !== 'object') { 7560 return null; 7561 } 7562 7563 const offeredQuote = raw.OfferedQuote || raw.offeredQuote || null; 7564 const hasOfferedQuote = offeredQuote && typeof offeredQuote === 'object'; 7565 const eligibilityOffered = resolveEligibilityOfferedFromResultRow(raw); 7566 if (!hasOfferedQuote && !eligibilityOffered) { 7567 return null; 7568 } 7569 7570 const quoteRoot = hasOfferedQuote ? offeredQuote : {}; 7571 const productQuote = quoteRoot.ProductQuote || quoteRoot.productQuote || {}; 7572 const figures = productQuote.Figures || productQuote.figures || {}; 7573 const asset = figures.Asset || figures.asset || quoteRoot.Asset || quoteRoot.asset || {}; 7574 const schedules = pickPaymentSchedules(figures); 7575 const regularSchedule = pickRegularSchedule(schedules); 7576 const regularPayment = resolveRegularPaymentFromFigures(figures, regularSchedule); 7577 const balloon = toFiniteNumberOrNull(figures.Balloon) ?? toFiniteNumberOrNull(figures.balloon); 7578 const financeType = resolveFinanceTypeFromResult(quoteRoot, productQuote, balloon); 7579 const funder = quoteRoot.Funder || quoteRoot.funder || {}; 7580 const funderCode = firstNonBlank(funder.Code, funder.code, productQuote.FunderCode, productQuote.funderCode); 7581 const productName = firstNonBlank(productQuote.ProductName, productQuote.productName); 7582 const lenderName = firstNonBlank(funder.Name, funder.name, productName, funderCode, 'Finance offer'); 7583 7584 return { 7585 OfferedQuote: offeredQuote, 7586 OriginalQuoteId: raw.OriginalQuoteId || raw.originalQuoteId || null, 7587 LenderData: raw.LenderData || raw.lenderData || null, 7588 PublicMessage: firstNonBlank(raw.PublicMessage, raw.publicMessage),
7589 Message: firstNonBlank(raw.Message, raw.message), 7590 Type: financeType, 7591 FunderCode: funderCode || '', 7592 ProductName: productName || lenderName, 7593 lenderName: String(lenderName || 'Finance offer'), 7594 QuoteUID: firstNonBlank( 7595 productQuote.QuoteId, 7596 productQuote.quoteId, 7597 productQuote.QuoteUID, 7598 productQuote.quoteUID, 7599 raw.OriginalQuoteId, 7600 raw.originalQuoteId, 7601 ), 7602 APR: toFiniteNumberOrNull(figures.APR) ?? toFiniteNumberOrNull(figures.apr), 7603 RegularPayment: regularPayment, 7604 Term: toFiniteNumberOrNull(figures.Term) ?? toFiniteNumberOrNull(figures.term), 7605 TotalPayable: 7606 toFiniteNumberOrNull(figures.TotalPayable) ?? toFiniteNumberOrNull(figures.totalPayable), 7607 CashPrice: 7608 toFiniteNumberOrNull(figures.TotalCashPrice) ?? 7609 toFiniteNumberOrNull(figures.totalCashPrice), 7610 Deposit: 7611 toFiniteNumberOrNull(figures.TotalDeposit) ?? 7612 toFiniteNumberOrNull(figures.totalDeposit) ?? 7613 toFiniteNumberOrNull(asset.CashDeposit), 7614 CreditAmount: 7615 toFiniteNumberOrNull(figures.Advance) ?? toFiniteNumberOrNull(figures.advance), 7616 OptionToPurchaseFee: 7617 toFiniteNumberOrNull(figures.OptionToPurchaseFee) ?? 7618 toFiniteNumberOrNull(figures.optionToPurchaseFee), 7619 FirstPayment: 7620 toFiniteNumberOrNull(figures.FirstPayment) ?? 7621 toFiniteNumberOrNull(figures.firstPayment) ?? 7622 regularPayment, 7623 FollowedByPayment: regularPayment, 7624 FinalPayment: 7625 toFiniteNumberOrNull(figures.FinalPayment) ?? 7626 toFiniteNumberOrNull(figures.finalPayment) ?? 7627 regularPayment, 7628 InterestCharges: 7629 toFiniteNumberOrNull(figures.InterestCharges) ?? 7630 toFiniteNumberOrNull(figures.interestCharges), 7631 AnnualFixedRate: 7632 toFiniteNumberOrNull(figures.InterestRate) ?? 7633 toFiniteNumberOrNull(figures.interestRate) ?? 7634 toFiniteNumberOrNull(figures.FlatRate) ?? 7635 toFiniteNumberOrNull(figures.flatRate), 7636 EligibilityOffered: eligibilityOffered, 7637 EligibilityCategory: eligibilityOffered, 7638 }; 7639 }; 7640 7641 const normalizeFinanceEligibilityQuotes = (arr) => { 7642 if (!Array.isArray(arr)) { 7643 return []; 7644 } 7645 return arr.map(normalizeFinanceEligibilityQuote).filter(Boolean); 7646 }; 7647 7648 /** API `results` only (Data.Results). Never iVendi Quotes[]. */ 7649 const extractFinanceEligibilityRawQuotes = (scanResponse) => { 7650 const src = scanResponse || {}; 7651 if (Array.isArray(src.results) && src.results.length > 0) { 7652 return src.results; 7653 } 7654 if (Array.isArray(src.Results) && src.Results.length > 0) { 7655 return src.Results; 7656 } 7657 return []; 7658 }; 7659 7660 const sortEligibilityQuotes = (mode, quotes) => { 7661 const arr = [...quotes]; 7662 if (mode === 'payment') { 7663 arr.sort((a, b) => (Number(a.RegularPayment) || 0) - (Number(b.RegularPayment) || 0)); 7664 } else if (mode === 'apr') { 7665 arr.sort((a, b) => (Number(a.APR) || 0) - (Number(b.APR) || 0)); 7666 } else { 7667 const order = ['pre-approved', 'high', 'alternative-offer', 'medium', 'low', 'unable', 'unknown']; 7668 const rank = (q) => { 7669 const k = resolveFeEligibilityCategoryKey(q); 7670 const ix = order.indexOf(k); 7671 return ix >= 0 ? ix : 99; 7672 }; 7673 arr.sort((a, b) => { 7674 const ra = rank(a); 7675 const rb = rank(b); 7676 if (ra !== rb) { 7677 return ra - rb; 7678 } 7679 return (Number(a.RegularPayment) || 0) - (Number(b.RegularPayment) || 0); 7680 }); 7681 } 7682 return arr; 7683 }; 7684 7685 const parseQuoteFromResultsCard = (card) => { 7686 if (!card?.dataset?.feQuoteJson) { 7687 return null; 7688 } 7689 try { 7690 return JSON.parse(card.dataset.feQuoteJson); 7691 } catch { 7692 return null; 7693 } 7694 }; 7695 7696 const escapeHtml = (s) => 7697 String(s) 7698 .replace(/&/g, '&') 7699 .replace(/</g, '<') 7700 .replace(/>/g, '>') 7701 .replace(/"/g, '"'); 7702 7703 const lenderDetailRowHtml = (label, value) => { 7704 const v = value == null || value === '' ? 'â' : String(value); 7705 return `<div class="cars-fe-lender-detail__breakdown-row"><span class="cars-fe-lender-detail__breakdown-label">${escapeHtml(label)}</span><span class="cars-fe-lender-detail__breakdown-value">${escapeHtml(v)}</span></div>`; 7706 }; 7707 7708 /** Figma BMW UX node 40000462:28429 â summary metric rows (distinct from quote breakdown list). */ 7709 const lenderDetailSummaryMetricRowHtml = (label, value) => { 7710 const v = value == null || value === '' ? 'â' : String(value); 7711 return `<div class="cars-fe-lender-detail__summary-metric-row"><span class="cars-fe-lender-detail__summary-label">${escapeHtml(label)}</span><span class="cars-fe-lender-detail__summary-value">${escapeHtml(v)}</span></div>`; 7712 }; 7713 7714 const lenderDetailSummaryDividerHtml = () => 7715 '<div class="cars-fe-lender-detail__summary-divider" aria-hidden="true"></div>'; 7716 7717 const closeLenderDetailFromResults = () => { 7718 const layer = document.getElementById('cars-fe-results-lender-detail'); 7719 if (!layer) { 7720 return; 7721 } 7722 layer.hidden = true; 7723 layer.setAttribute('aria-hidden', 'true'); 7724 syncFinanceEligibilityJourneyUrl(); 7725 }; 7726 7727 /** CMS /finance/ story: no listing on panel (see data-fe-hide-listing). */ 7728 const isFeFinanceStoryContext = () => panel?.getAttribute('data-fe-hide-listing') === 'true'; 7729 /** Lender detail footer: "Apply now" (Ivendi) vs alternate CTA (test-drive or VLP on finance page). */ 7730 // Grouped variant (Figma 40003936:37009) uses a single "More info" CTA on every result card. 7731 const feResultsCardCtaLabelForVariant = (variant) => { 7732 if (isGroupedFormActive()) { 7733 return 'More info'; 7734 } 7735 return variant === 'book' ? 'More info' : 'More info and Apply'; 7736 }; 7737 const syncFeResultsCardCtaLabel = (card, variant) => { 7738 const btn = card?.querySelector?.('.cars-fe-results-card__cta-btn'); 7739 if (btn) { 7740 btn.textContent = feResultsCardCtaLabelForVariant(variant); 7741 } 7742 }; 7743
7744 const setLenderDetailFooterCtaVisible = (el, show) => { 7745 if (!el) { 7746 return; 7747 } 7748 if (show) { 7749 el.hidden = false; 7750 el.removeAttribute('hidden'); 7751 el.setAttribute('aria-hidden', 'false'); 7752 } else { 7753 el.hidden = true; 7754 el.setAttribute('hidden', ''); 7755 el.setAttribute('aria-hidden', 'true'); 7756 } 7757 }; 7758 7759 const syncLenderDetailFooterCta = (variant, sourceCard) => { 7760 const useBook = variant === 'book'; 7761 const applyEl = document.getElementById('cars-fe-lender-detail-apply'); 7762 const bookEl = document.getElementById('cars-fe-lender-detail-book-appointment'); 7763 setLenderDetailFooterCtaVisible(applyEl, !useBook); 7764 setLenderDetailFooterCtaVisible(bookEl, useBook); 7765 if (sourceCard) { 7766 syncFeResultsCardCtaLabel(sourceCard, variant); 7767 } 7768 }; 7769 const resolveLenderDetailFooterCtaVariant = (quote) => { 7770 if ( 7771 !isFeFinanceStoryContext() && 7772 typeof window.getFinanceEligibilityLenderDetailFooterCta === 'function' 7773 ) { 7774 const fromPosthog = window.getFinanceEligibilityLenderDetailFooterCta(); 7775 if (fromPosthog === 'apply' || fromPosthog === 'book') { 7776 return fromPosthog; 7777 } 7778 } 7779 const explicit = String(quote?.lenderDetailFooterCta || quote?.feLenderDetailFooterCta || '') 7780 .trim() 7781 .toLowerCase(); 7782 if (explicit === 'book' || explicit === 'appointment') { 7783 return 'book'; 7784 } 7785 if (explicit === 'apply') { 7786 return 'apply'; 7787 } 7788 if (isFeFinanceStoryContext()) { 7789 return 'book'; 7790 } 7791 return 'apply'; 7792 }; 7793 7794 const populateLenderDetailLayer = (quote, sourceCard) => { 7795 const titleEl = document.getElementById('cars-fe-lender-detail-title'); 7796 const badgeText = document.getElementById('cars-fe-lender-detail-badge-text'); 7797 const brandEl = document.getElementById('cars-fe-lender-detail-brand'); 7798 const summaryType = document.getElementById('cars-fe-lender-detail-summary-type'); 7799 const summaryRows = document.getElementById('cars-fe-lender-detail-summary-rows'); 7800 const breakdownEl = document.getElementById('cars-fe-lender-detail-breakdown'); 7801 const legalEl = document.getElementById('cars-fe-lender-detail-legal'); 7802 if (!quote || !titleEl || !badgeText || !brandEl || !summaryType || !summaryRows || !breakdownEl) { 7803 return; 7804 } 7805 const lenderName = (quote.lenderName || quote.ProductName || quote.FunderCode || 'Finance offer').trim(); 7806 titleEl.textContent = `Finance Provided by: ${lenderName}`; 7807 if (legalEl) { 7808 legalEl.textContent = 7809 'Credit is subject to status and approval and is only available to UK residents aged 18 and over. ' + 7810 `Credit is provided by ${lenderName}. ` + 7811 `${lenderName} is authorised and regulated by the Financial Conduct Authority.`; 7812 } 7813 const catKey = quote.eligibilityCategoryKey || resolveFeEligibilityCategoryKey(quote); 7814 const label = getFeEligibilityBadgeLabel(quote); 7815 const badgeEl = document.getElementById('cars-fe-lender-detail-badge'); 7816 badgeText.textContent = label; 7817 syncFeEligibilityCategoryPillClass(badgeEl, catKey); 7818 if (badgeEl) { 7819 badgeEl.hidden = false; 7820 badgeEl.setAttribute('aria-hidden', 'false'); 7821 } 7822 7823 const typeStr = String(quote.Type || '').toUpperCase(); 7824 const isPcp = typeStr === 'PCP'; 7825 summaryType.textContent = isPcp ? 'Personal Contract Purchase' : 'Hire Purchase'; 7826 7827 brandEl.textContent = ''; 7828 brandEl.hidden = true; 7829 if (quote.LenderLogoUrl) { 7830 const im = document.createElement('img'); 7831 im.src = quote.LenderLogoUrl; 7832 im.alt = quote.LenderLogoAlt || lenderName; 7833 brandEl.appendChild(im); 7834 brandEl.hidden = false; 7835 } 7836 7837 const termM = Number(quote.Term); 7838 const termLabel = Number.isFinite(termM) ? `${Math.round(termM)} months` : 'â'; 7839 const termSummaryMonthsOnly = Number.isFinite(termM) ? String(Math.round(termM)) : 'â'; 7840 const rp = formatFeMoney(quote.RegularPayment); 7841 const apr = formatFeApr(quote.APR); 7842 const tp = formatFeMoney(quote.TotalPayable); 7843 const ft = typeStr || 'â'; 7844 7845 const d = lenderDetailSummaryDividerHtml; 7846 const summaryHtml = [ 7847 lenderDetailSummaryMetricRowHtml('Finance type', ft), 7848 d(), 7849 lenderDetailSummaryMetricRowHtml('Monthly Payments', rp), 7850 d(), 7851 `<div class="cars-fe-lender-detail__summary-apr-wrap"><div class="cars-fe-lender-detail__rate-pill">Your Personalised Rate</div>${lenderDetailSummaryMetricRowHtml('APR', apr)}</div>`, 7852 d(), 7853 lenderDetailSummaryMetricRowHtml('Agreement Term', termSummaryMonthsOnly), 7854 d(), 7855 lenderDetailSummaryMetricRowHtml('Total Payable', tp), 7856 ].join(''); 7857 summaryRows.innerHTML = summaryHtml; 7858 7859 const fmt = (n) => (n != null && Number.isFinite(Number(n)) ? formatFeMoney(n) : 'â'); 7860 const n = (v) => (v != null && Number.isFinite(Number(v)) ? Number(v) : null); 7861 const termInt = n(quote.Term); 7862 const midCount = 7863 termInt != null && termInt > 2 ? Math.max(0, Math.round(termInt) - 2) : null; 7864 const rows = [ 7865 ['Cash Price', fmt(quote.CashPrice)], 7866 ['Total Deposit (Details)', fmt(quote.Deposit)], 7867 ['Total Amount of Credit', fmt(quote.CreditAmount)], 7868 ['Agreement Term', termLabel], 7869 ['Option to Purchase Fee', fmt(quote.OptionToPurchaseFee)], 7870 ['First Payment', fmt(quote.FirstPayment ?? quote.RegularPayment)], 7871 [ 7872 midCount != null && midCount > 0 ? `Followed by ${midCount} Payments` : 'Followed by payments', 7873 fmt(quote.FollowedByPayment ?? quote.RegularPayment), 7874 ], 7875 ['Final Payment', fmt(quote.FinalPayment ?? quote.RegularPayment)], 7876 ['Interest Charges', fmt(quote.InterestCharges)], 7877 ['Annual Fixed Interest Rate', quote.AnnualFixedRate != null ? `${Number(quote.AnnualFixedRate).toFixed(2)}%` : 'â'], 7878 ['APR', apr], 7879 ['Total Amount Payable', tp], 7880 ]; 7881 breakdownEl.innerHTML = rows.map(([lab, val]) => lenderDetailRowHtml(lab, val)).join(''); 7882 syncLenderDetailFooterCta(resolveLenderDetailFooterCtaVariant(quote), sourceCard); 7883 }; 7884 7885 const openLenderDetailFromResults = (card) => { 7886 const layer = document.getElementById('cars-fe-results-lender-detail'); 7887 if (!layer) { 7888 return; 7889 } 7890 closeFinanceEligibilityResultsGlossary(); 7891 const quote = parseQuoteFromResultsCard(card); 7892 if (!quote) { 7893 return; 7894 } 7895 populateLenderDetailLayer(quote, card); 7896 layer.hidden = false;
7897 layer.setAttribute('aria-hidden', 'false'); 7898 document.getElementById('cars-fe-lender-detail-close')?.focus?.(); 7899 syncFinanceEligibilityJourneyUrl(); 7900 }; 7901 7902 const renderEligibilityResultCards = (quotes) => { 7903 if (!resultsCardsMount) { 7904 return; 7905 } 7906 resultsCardsMount.textContent = ''; 7907 quotes.forEach((quote) => { 7908 if (!quote) { 7909 return; 7910 } 7911 const catKey = resolveFeEligibilityCategoryKey(quote); 7912 const badgeLabel = getFeEligibilityBadgeLabel(quote); 7913 const lenderName = quote.lenderName || quote.ProductName || quote.FunderCode || 'Finance offer'; 7914 7915 const card = document.createElement('article'); 7916 card.className = 'cars-fe-results-card'; 7917 card.dataset.feLenderName = lenderName; 7918 7919 const head = document.createElement('div'); 7920 head.className = 'cars-fe-results-card__head'; 7921 const badge = document.createElement('div'); 7922 badge.className = 'cars-fe-results-card__badge'; 7923 syncFeEligibilityCategoryPillClass(badge, catKey); 7924 const badgeText = document.createElement('span'); 7925 badgeText.textContent = badgeLabel; 7926 badge.appendChild(badgeText); 7927 const badgeIcon = document.createElement('span'); 7928 badgeIcon.className = 'cars-fe-results-card__badge-icon'; 7929 badgeIcon.setAttribute('role', 'button'); 7930 badgeIcon.setAttribute('tabindex', '0'); 7931 badgeIcon.setAttribute('aria-label', 'Open product details'); 7932 badge.appendChild(badgeIcon); 7933 head.appendChild(badge); 7934 7935 const brand = document.createElement('div'); 7936 brand.className = 'cars-fe-results-card__brand'; 7937 if (quote.LenderLogoUrl) { 7938 const img = document.createElement('img'); 7939 img.className = 'cars-fe-results-card__brand-img'; 7940 img.src = quote.LenderLogoUrl; 7941 img.alt = quote.LenderLogoAlt || lenderName; 7942 img.loading = 'lazy'; 7943 brand.appendChild(img); 7944 } else { 7945 brand.classList.add('cars-fe-results-card__brand--text'); 7946 const lender = document.createElement('div'); 7947 lender.className = 'cars-fe-results-card__lender'; 7948 lender.textContent = lenderName; 7949 brand.appendChild(lender); 7950 } 7951 head.appendChild(brand); 7952 card.appendChild(head); 7953 7954 const rows = document.createElement('div'); 7955 rows.className = 'cars-fe-results-card__rows'; 7956 7957 const typeStr = String(quote.Type || '').toUpperCase(); 7958 const termMonths = Number(quote.Term); 7959 const rowSpecs = [ 7960 ['Finance type', typeStr || 'â'], 7961 ['Monthly Payments', formatFeMoney(quote.RegularPayment)], 7962 // Grouped variant (Figma 40003936:37009) spells out that the APR is indicative. 7963 [isGroupedFormActive() ? 'APR (indicative)' : 'APR', formatFeApr(quote.APR)], 7964 ['Agreement Term', Number.isFinite(termMonths) ? String(Math.round(termMonths)) : 'â'], 7965 ['Total Payable', formatFeMoney(quote.TotalPayable)], 7966 ]; 7967 rowSpecs.forEach(([label, value], idx) => { 7968 const row = document.createElement('div'); 7969 row.className = 'cars-fe-results-card__row'; 7970 const lab = document.createElement('div'); 7971 lab.className = 'cars-fe-results-card__label'; 7972 lab.textContent = label; 7973 const valCell = document.createElement('div'); 7974 valCell.className = 'cars-fe-results-card__value'; 7975 valCell.textContent = value; 7976 row.appendChild(lab); 7977 row.appendChild(valCell); 7978 rows.appendChild(row); 7979 if (idx < rowSpecs.length - 1) { 7980 const rule = document.createElement('div'); 7981 rule.className = 'cars-fe-results-card__rule'; 7982 rows.appendChild(rule); 7983 } 7984 }); 7985 7986 card.appendChild(rows); 7987 7988 const ctaWrap = document.createElement('div'); 7989 ctaWrap.className = 'cars-fe-results-card__cta'; 7990 const cta = document.createElement('button'); 7991 cta.type = 'button'; 7992 cta.className = 'btn primary rounded cars-fe-results-card__cta-btn'; 7993 const footerVariant = resolveLenderDetailFooterCtaVariant(quote); 7994 cta.textContent = feResultsCardCtaLabelForVariant(footerVariant); 7995 ctaWrap.appendChild(cta); 7996 card.appendChild(ctaWrap); 7997 7998 try { 7999 card.dataset.feQuoteJson = JSON.stringify({ 8000 ...quote, 8001 lenderName, 8002 badgeLabel, 8003 eligibilityCategoryKey: catKey, 8004 }); 8005 } catch { 8006 /* ignore non-serializable */ 8007 } 8008 8009 resultsCardsMount.appendChild(card); 8010 }); 8011 }; 8012 8013 const showFinanceResultsModalChrome = () => { 8014 if (!resultsModal) { 8015 return; 8016 } 8017 closeFinanceEligibilityResultsGlossary(); 8018 syncResultsVehicleStripFromPanel(); 8019 resultsModal.hidden = false;
8020 resultsModal.setAttribute('aria-hidden', 'false'); 8021 if (resultsModalStatus) { 8022 resultsModalStatus.hidden = true; 8023 resultsModalStatus.textContent = ''; 8024 } 8025 syncResultsModalEmptyState(true); 8026 syncFinanceEligibilityJourneyUrl(); 8027 }; 8028 8029 const openFinanceEligibilityResultsModal = () => { 8030 if (!resultsModal) { 8031 return; 8032 } 8033 const resultsDemo = 8034 panel && String(panel.getAttribute('data-fe-eligibility-results-demo') || '').toLowerCase() === 'true'; 8035 if (!resultsDemo) { 8036 return; 8037 } 8038 showFinanceResultsModalChrome(); 8039 syncFinanceEligibilityJourneyUrl('submit-clicked'); 8040 8041 const financeResultsMinLoadMs = 2000; 8042 const financeResultsMinLoadDelay = () => new Promise((resolve) => setTimeout(resolve, financeResultsMinLoadMs)); 8043 8044 if (resultsCardsMount) { 8045 resultsCardsMount.textContent = ''; 8046 } 8047 setFinanceResultsModalLoading(true); 8048 financeResultsMinLoadDelay().then(() => { 8049 setFinanceResultsModalLoading(false); 8050 if (!eligibilityQuotesCache.length) { 8051 syncResultsModalEmptyState(false); 8052 return; 8053 } 8054 syncResultsModalEmptyState(true); 8055 const mode = resultsSortSelect && resultsSortSelect.value ? resultsSortSelect.value : 'eligibility'; 8056 renderEligibilityResultCards(sortEligibilityQuotes(mode, eligibilityQuotesCache)); 8057 }); 8058 }; 8059 8060 const collectFinanceEligibilityQuoteIds = () => { 8061 const fromVdpTab = getVdpFinanceQuoteIdsFromTabContainer(document.querySelector('.tab-content-container')); 8062 if (fromVdpTab.length > 0) { 8063 return fromVdpTab; 8064 } 8065 try { 8066 const raw = panel?.getAttribute('data-fe-finance-eligibility-quote-ids'); 8067 if (raw) { 8068 const parsed = JSON.parse(raw); 8069 if (Array.isArray(parsed)) { 8070 return parsed.filter((x) => typeof x === 'string' && x.trim().length > 0); 8071 } 8072 } 8073 } catch { 8074 /* ignore */ 8075 } 8076 return []; 8077 }; 8078 8079 const resolveFinanceEligibilitySiteId = () => { 8080 const n = parseInt(panel?.getAttribute('data-fe-site-id') || '0', 10); 8081 return Number.isNaN(n) || n <= 0 ? 0 : n; 8082 }; 8083 8084 const resolveFinanceEligibilityDealershipId = () => { 8085 // API field is dealershipId; Ivendi Connect Finance Scan expects IvendiRetailerId (not Quoteware IvendiDealerID). 8086 return String(panel?.getAttribute('data-fe-ivendi-retailer-id') || '').trim(); 8087 }; 8088 8089 const eligibilityPollMaxMs = 90000; 8090 const eligibilityPollIntervalMs = 2000; 8091 8092 const isFinanceScanStatusPending = (scanStatus) => { 8093 const s = String(scanStatus || '').trim().toLowerCase(); 8094 if (!s) { 8095 return true; 8096 } 8097 return ( 8098 s === 'pending' || 8099 s === 'processing' || 8100 s.includes('process') || 8101 s.includes('progress') || 8102 s === 'queued' 8103 ); 8104 }; 8105 8106 const isFinanceScanStatusTerminal = (scanStatus) => { 8107 const s = String(scanStatus || '').trim().toLowerCase(); 8108 if (!s) { 8109 return false; 8110 } 8111 if (isFinanceScanStatusPending(s)) { 8112 return false; 8113 } 8114 const failHints = ['fail', 'error', 'cancel', 'reject', 'timeout', 'timed out', 'invalid']; 8115 if (failHints.some((h) => s.includes(h))) { 8116 return true; 8117 } 8118 const doneHints = ['complete', 'finished', 'done', 'succeeded', 'success']; 8119 return doneHints.some((h) => s === h || s.startsWith(h)); 8120 }; 8121 8122 /** 8123 * Keep polling while Status is Pending/Processing even if some Results already arrived. 8124 * When we know how many quoteIds were submitted, also wait until results.length >= that count 8125 * (iVendi returns one Results row per quoteId). 8126 */ 8127 const shouldStopFinanceScanClientPoll = ( 8128 resultsLength, 8129 scanStatus, 8130 elapsedMs, 8131 maxMs, 8132 expectedResultsCount = 0, 8133 ) => { 8134 if (elapsedMs >= maxMs) { 8135 return true; 8136 } 8137 const expected = Number(expectedResultsCount) || 0; 8138 if (expected > 0 && resultsLength >= expected) { 8139 return true; 8140 } 8141 return isFinanceScanStatusTerminal(scanStatus); 8142 }; 8143 8144 const runFeScanClientPoll = async ( 8145 retailerId, 8146 scanId, 8147 maxMs, 8148 intervalMs, 8149 generation, 8150 expectedResultsCount = 0, 8151 ) => { 8152 let list = []; 8153 const start = Date.now(); 8154 while (true) { 8155 if (generation !== feScanPollGeneration) { 8156 return { list, cancelled: true }; 8157 }
8158 const elapsed = Date.now() - start; 8159 let poll = null; 8160 try { 8161 poll = await API.Stock.PollFinanceEligibility(retailerId, scanId); 8162 } catch { 8163 poll = null; 8164 } 8165 if (poll != null) { 8166 setFinanceScanLoadingRetryVisible(false); 8167 } else { 8168 setFinanceScanLoadingRetryVisible(true); 8169 } 8170 const rawResults = extractFinanceEligibilityRawQuotes(poll); 8171 list = normalizeFinanceEligibilityQuotes(rawResults); 8172 const st = poll?.scanStatus; 8173 if ( 8174 shouldStopFinanceScanClientPoll( 8175 rawResults.length, 8176 st, 8177 elapsed, 8178 maxMs, 8179 expectedResultsCount, 8180 ) 8181 ) { 8182 break; 8183 } 8184 await new Promise((r) => setTimeout(r, intervalMs)); 8185 } 8186 return { list, cancelled: false }; 8187 }; 8188 8189 /** Success copy is variant-specific (Figma 40003936:37009 for grouped); empty/error copy is shared. */ 8190 const resultsIntroSubCopy = (introSub, hasQuotes, isError) => { 8191 if (hasQuotes) { 8192 const grouped = panel?.dataset.feFormDensity === 'grouped'; 8193 const attr = grouped ? 'data-fe-results-sub-grouped' : 'data-fe-results-sub-control'; 8194 return introSub.getAttribute(attr) || 'Find your perfect finance option.'; 8195 } 8196 if (isError) { 8197 return 'We couldnât complete your finance check with the details provided.'; 8198 } 8199 return 'No finance products matched with your current details.'; 8200 }; 8201 8202 const syncResultsModalEmptyState = (hasQuotes, options = {}) => { 8203 const toolbar = resultsModal?.querySelector('.cars-fe-results-modal__toolbar'); 8204 const introSub = resultsModal?.querySelector('.cars-fe-results-modal__intro-sub'); 8205 const bookBtn = document.getElementById('cars-fe-results-book-appointment'); 8206 const updateBtn = document.getElementById('cars-fe-results-update-details'); 8207 const isError = Boolean(options.isError); 8208 if (resultsModalStatus) { 8209 resultsModalStatus.hidden = true; 8210 resultsModalStatus.textContent = ''; 8211 } 8212 if (toolbar) { 8213 toolbar.hidden = !hasQuotes; 8214 } 8215 if (introSub) { 8216 introSub.textContent = resultsIntroSubCopy(introSub, hasQuotes, isError); 8217 } 8218 if (bookBtn) { 8219 bookBtn.hidden = !hasQuotes; 8220 } 8221 if (updateBtn) { 8222 updateBtn.hidden = hasQuotes; 8223 } 8224 // The grouped variant's secondary link belongs with the primary CTA: with no results the 8225 // footer offers "Update your details" alone, so it must not linger beside it. 8226 const continueSearchingLink = document.getElementById('cars-fe-results-continue-searching'); 8227 if (continueSearchingLink) { 8228 continueSearchingLink.hidden = !hasQuotes; 8229 } 8230 if (!hasQuotes && resultsCardsMount) { 8231 resultsCardsMount.textContent = ''; 8232 const empty = document.createElement('div'); 8233 empty.id = 'cars-fe-results-modal-empty'; 8234 empty.className = 'cars-fe-results-modal__empty'; 8235 empty.setAttribute('role', 'status'); 8236 const title = document.createElement('p'); 8237 title.className = 'cars-fe-results-modal__empty-title'; 8238 title.textContent = 'Unable to retrieve any finance results'; 8239 const body = document.createElement('p'); 8240 body.className = 'cars-fe-results-modal__empty-body'; 8241 body.textContent = 'Please check your details and try again'; 8242 empty.appendChild(title); 8243 empty.appendChild(body); 8244 resultsCardsMount.appendChild(empty); 8245 } 8246 }; 8247 8248 /** Scan/API failures: hide raw errors and offer Update your details instead. */ 8249 const showResultsUpdateDetailsFallback = () => { 8250 eligibilityQuotesCache = []; 8251 setFinanceResultsModalLoading(false); 8252 syncResultsModalEmptyState(false, { isError: true }); 8253 }; 8254 8255 const applyFinanceEligibilityScanPollResult = (list) => { 8256 eligibilityQuotesCache = list; 8257 if (list.length === 0) { 8258 syncResultsModalEmptyState(false); 8259 } else { 8260 syncResultsModalEmptyState(true); 8261 const mode = resultsSortSelect && resultsSortSelect.value ? resultsSortSelect.value : 'eligibility'; 8262 renderEligibilityResultCards(sortEligibilityQuotes(mode, list)); 8263 } 8264 setFinanceResultsModalLoading(false); 8265 }; 8266 8267 document.getElementById('cars-fe-results-loading-retry')?.addEventListener('click', async (e) => { 8268 e.preventDefault(); 8269 const ctx = feScanPollRetryContext; 8270 if ( 8271 !ctx?.retailerId || 8272 !ctx?.scanId || 8273 typeof API === 'undefined' || 8274 !API.Stock || 8275 typeof API.Stock.PollFinanceEligibility !== 'function' 8276 ) { 8277 return; 8278 } 8279 feScanPollGeneration += 1; 8280 const pollGen = feScanPollGeneration; 8281 const btn = e.currentTarget; 8282 btn.disabled = true; 8283 setFinanceScanLoadingRetryVisible(false); 8284 try { 8285 const { list, cancelled } = await runFeScanClientPoll( 8286 ctx.retailerId, 8287 ctx.scanId, 8288 eligibilityPollMaxMs, 8289 eligibilityPollIntervalMs, 8290 pollGen, 8291 ctx.expectedResultsCount || 0, 8292 ); 8293 if (cancelled) { 8294 return; 8295 } 8296 applyFinanceEligibilityScanPollResult(list); 8297 } catch (err) { 8298 showResultsUpdateDetailsFallback(); 8299 } finally { 8300 btn.disabled = false;
8301 } 8302 }); 8303 8304 reviewViewResultsBtn?.addEventListener('click', (e) => { 8305 e.preventDefault(); 8306 if ( 8307 reviewViewResultsBtn.getAttribute('aria-disabled') === 'true' || 8308 reviewViewResultsBtn.classList.contains('cars-fe-next--disabled') 8309 ) { 8310 if (!reviewConsentCb || !reviewConsentCb.checked) { 8311 syncReviewConsentError(true); 8312 const consentRow = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__row'); 8313 if (consentRow) { 8314 financeEligibilityScheduleFieldScrollIntoView(consentRow, { 8315 behavior: 'smooth', 8316 topPaddingPx: 16, 8317 }); 8318 } 8319 reviewConsentCb?.focus?.(); 8320 } 8321 return; 8322 } 8323 if (!reviewConsentCb || !reviewConsentCb.checked) { 8324 syncReviewConsentError(true); 8325 const consentRow = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__row'); 8326 if (consentRow) { 8327 financeEligibilityScheduleFieldScrollIntoView(consentRow, { 8328 behavior: 'smooth', 8329 topPaddingPx: 16, 8330 }); 8331 } 8332 reviewConsentCb?.focus?.(); 8333 return; 8334 } 8335 syncReviewConsentError(false); 8336 const journey = collectJourneyDetailPayload(); 8337 const resultsDemo = 8338 panel && String(panel.getAttribute('data-fe-eligibility-results-demo') || '').toLowerCase() === 'true'; 8339 if (!resultsDemo) { 8340 const ivendiFieldErrors = getFinanceEligibilityIvendiFieldErrors(journey); 8341 if (ivendiFieldErrors.length > 0) { 8342 window.alert(ivendiFieldErrors.join('\n')); 8343 return; 8344 } 8345 const panelStockId = panel.getAttribute('data-fe-stock-id') || ''; 8346 if (!resolveFinanceEligibilityDealershipId()) { 8347 window.alert('Retailer ID is required before submitting a finance eligibility scan.'); 8348 return; 8349 } 8350 if (!String(panelStockId).trim()) { 8351 window.alert('Stock is required before submitting a finance eligibility scan.'); 8352 return; 8353 } 8354 if (collectFinanceEligibilityQuoteIds().length === 0) { 8355 window.alert( 8356 'A finance quote ID is required. Generate or select a finance quote for this vehicle, then try again.', 8357 ); 8358 return; 8359 } 8360 } 8361 window.dispatchEvent( 8362 new CustomEvent('cars-finance-eligibility-panel-complete', { 8363 bubbles: true, 8364 detail: journey, 8365 }), 8366 ); 8367 8368 if (resultsDemo) { 8369 openFinanceEligibilityResultsModal(); 8370 return; 8371 } 8372 8373 showFinanceResultsModalChrome(); 8374 syncFinanceEligibilityJourneyUrl('submit-clicked'); 8375 8376 const stockId = panel.getAttribute('data-fe-stock-id'); 8377 const term = parseInt(panel.getAttribute('data-fe-finance-term') || '48', 10) || 48; 8378 if ( 8379 !stockId || 8380 typeof API === 'undefined' || 8381 !API.Stock || 8382 typeof API.Stock.SubmitFinanceEligibility !== 'function' || 8383 typeof API.Stock.PollFinanceEligibility !== 'function' 8384 ) { 8385 showResultsUpdateDetailsFallback(); 8386 return; 8387 } 8388 8389 setFinanceResultsModalLoading(true); 8390 if (resultsCardsMount) { 8391 resultsCardsMount.textContent = ''; 8392 } 8393 8394 const quoteIds = collectFinanceEligibilityQuoteIds(); 8395 const scanRequestPayload = { 8396 stockId: parseInt(stockId, 10), 8397 term, 8398 journey, 8399 dealershipId: resolveFinanceEligibilityDealershipId(), 8400 webhookCallbackUrl: '', 8401 quoteIds, 8402 clientPoll: true, 8403 }; 8404 8405 (async () => { 8406 try { 8407 const res = await API.Stock.SubmitFinanceEligibility(scanRequestPayload); 8408 let list = normalizeFinanceEligibilityQuotes(extractFinanceEligibilityRawQuotes(res)); 8409 const retailerForPoll = res && res.retailerId; 8410 const financeScanId = res && res.financeScanId; 8411 const expectedResultsCount = quoteIds.length; 8412 8413 if (list.length === 0 && financeScanId && retailerForPoll && res?.clientPoll) { 8414 feScanPollRetryContext = { 8415 retailerId: retailerForPoll, 8416 scanId: financeScanId, 8417 expectedResultsCount, 8418 }; 8419 feScanPollGeneration += 1; 8420 const pollGen = feScanPollGeneration; 8421 const { list: polled, cancelled } = await runFeScanClientPoll( 8422 retailerForPoll, 8423 financeScanId, 8424 eligibilityPollMaxMs, 8425 eligibilityPollIntervalMs, 8426 pollGen, 8427 expectedResultsCount, 8428 ); 8429 if (cancelled) { 8430 return; 8431 } 8432 list = polled; 8433 } else { 8434 feScanPollRetryContext = null; 8435 } 8436 8437 applyFinanceEligibilityScanPollResult(list); 8438 } catch (err) { 8439 showResultsUpdateDetailsFallback(); 8440 } 8441 })(); 8442 }); 8443 8444 reviewConsentCb?.addEventListener('change', () => { 8445 validateReviewConsent(); 8446 if (reviewStep && reviewStep.hidden === false) { 8447 syncFinanceEligibilityJourneyUrl(); 8448 } 8449 }); 8450 8451 document.getElementById('cars-fe-results-modal-close')?.addEventListener('click', (e) => { 8452 e.preventDefault(); 8453 closeFinanceEligibilityPanel(); 8454 }); 8455 8456 document.getElementById('cars-fe-results-continue-searching')?.addEventListener('click', (e) => { 8457 e.preventDefault(); 8458 closeFinanceEligibilityPanel(); 8459 }); 8460 8461 const closeFinanceEligibilityInfoOverlay = (overlay) => { 8462 if (!overlay || overlay.hidden) { 8463 return; 8464 } 8465 overlay.hidden = true; 8466 overlay.setAttribute('aria-hidden', 'true'); 8467 panel?.querySelector(`[data-fe-open-info-overlay="${overlay.id}"]`)?.focus?.(); 8468 }; 8469 8470 const openFinanceEligibilityInfoOverlay = (overlayId) => {
8471 const overlay = document.getElementById(overlayId); 8472 if (!overlay) { 8473 return; 8474 } 8475 overlay.hidden = false; 8476 overlay.setAttribute('aria-hidden', 'false'); 8477 overlay.querySelector('[data-fe-close-info-overlay]')?.focus?.(); 8478 }; 8479 8480 panel?.addEventListener('click', (e) => { 8481 const opener = e.target.closest?.('[data-fe-open-info-overlay]'); 8482 if (opener) { 8483 e.preventDefault(); 8484 openFinanceEligibilityInfoOverlay(opener.getAttribute('data-fe-open-info-overlay')); 8485 return; 8486 } 8487 const overlay = e.target.closest?.('.cars-fe-info-overlay'); 8488 if (!overlay) { 8489 return; 8490 } 8491 // The close button and the backdrop dismiss it; a click inside the sheet does not. 8492 const dismissed = 8493 e.target.closest?.('[data-fe-close-info-overlay]') || !e.target.closest?.('.cars-fe-info-overlay__sheet'); 8494 if (dismissed) { 8495 e.preventDefault(); 8496 closeFinanceEligibilityInfoOverlay(overlay); 8497 } 8498 }); 8499 8500 document.getElementById('cars-fe-results-glossary-open')?.addEventListener('click', (e) => { 8501 e.preventDefault(); 8502 openFinanceEligibilityResultsGlossary(); 8503 }); 8504 8505 document.getElementById('cars-fe-results-glossary-close')?.addEventListener('click', (e) => { 8506 e.preventDefault(); 8507 closeFinanceEligibilityResultsGlossary(); 8508 }); 8509 8510 document.getElementById('cars-fe-lender-detail-close')?.addEventListener('click', (e) => { 8511 e.preventDefault(); 8512 closeLenderDetailFromResults(); 8513 }); 8514 8515 document.getElementById('cars-fe-lender-detail-apply')?.addEventListener('click', (e) => { 8516 e.preventDefault(); 8517 if (typeof window.applyForFinanceFromVdp === 'function') { 8518 window.applyForFinanceFromVdp(); 8519 } 8520 }); 8521 8522 document.addEventListener('cars-fe-check-eligibility-journey-variant', () => { 8523 // The flag also decides form density (test-c), so re-apply it whatever screen is showing. 8524 applyFormDensityVariant(); 8525 const layer = document.getElementById('cars-fe-results-lender-detail'); 8526 if (!layer || layer.hidden) { 8527 return; 8528 } 8529 syncLenderDetailFooterCta(resolveLenderDetailFooterCtaVariant(null), null); 8530 }); 8531 8532 resultsModal?.addEventListener('click', (e) => { 8533 const badgeIcon = e.target.closest?.('.cars-fe-results-card__badge-icon'); 8534 const ctaBtn = e.target.closest?.('.cars-fe-results-card__cta-btn'); 8535 if (!badgeIcon && !ctaBtn) { 8536 return; 8537 } 8538 const card = (badgeIcon || ctaBtn).closest?.('.cars-fe-results-card'); 8539 if (!card || !resultsCardsMount?.contains(card)) { 8540 return; 8541 } 8542 e.preventDefault(); 8543 e.stopPropagation(); 8544 openLenderDetailFromResults(card); 8545 }); 8546 8547 resultsCardsMount?.addEventListener('keydown', (e) => { 8548 const icon = e.target.closest?.('.cars-fe-results-card__badge-icon'); 8549 if (!icon || !resultsCardsMount.contains(icon)) { 8550 return; 8551 } 8552 if (e.key !== 'Enter' && e.key !== ' ') { 8553 return; 8554 } 8555 e.preventDefault(); 8556 const card = icon.closest('.cars-fe-results-card'); 8557 openLenderDetailFromResults(card); 8558 }); 8559 8560 document.addEventListener('keydown', (e) => { 8561 if (e.key !== 'Escape') { 8562 return; 8563 } 8564 const infoOverlay = panel?.querySelector('.cars-fe-info-overlay:not([hidden])'); 8565 if (infoOverlay) { 8566 e.preventDefault(); 8567 closeFinanceEligibilityInfoOverlay(infoOverlay); 8568 return; 8569 } 8570 const glossaryLay = document.getElementById('cars-fe-results-glossary'); 8571 if (glossaryLay && glossaryLay.hidden === false) { 8572 e.preventDefault(); 8573 closeFinanceEligibilityResultsGlossary(); 8574 return; 8575 } 8576 const layer = document.getElementById('cars-fe-results-lender-detail'); 8577 if (!layer || layer.hidden) { 8578 return; 8579 } 8580 e.preventDefault(); 8581 closeLenderDetailFromResults(); 8582 }); 8583 8584 document.getElementById('cars-fe-results-modal-view-details')?.addEventListener('click', () => { 8585 closeFinanceEligibilityResultsModal(); 8586 jumpToReviewEdit('details'); 8587 }); 8588 8589 document.getElementById('cars-fe-results-update-details')?.addEventListener('click', (e) => { 8590 e.preventDefault(); 8591 closeFinanceEligibilityResultsModal(); 8592 showReviewStep(); 8593 }); 8594 8595 resultsSortSelect?.addEventListener('change', () => { 8596 if (!resultsSortSelect || eligibilityQuotesCache.length === 0) { 8597 return; 8598 } 8599 renderEligibilityResultCards(sortEligibilityQuotes(resultsSortSelect.value, eligibilityQuotesCache)); 8600 }); 8601 8602 panel.addEventListener('click', (e) => { 8603 const jumpBtn = e.target.closest('[data-cars-fe-review-jump]'); 8604 if (!jumpBtn || !reviewStep || reviewStep.hidden || !reviewStep.contains(jumpBtn)) { 8605 return; 8606 } 8607 e.preventDefault(); 8608 jumpToReviewEdit(jumpBtn.getAttribute('data-cars-fe-review-jump')); 8609 }); 8610 8611 document.getElementById('cars-fe-employment-details-save-button')?.addEventListener('click', (e) => { 8612 e.preventDefault(); 8613 if (!employmentFormValidForSave()) { 8614 requestFeStepFieldHighlights('employment'); 8615 applyEmploymentFormFieldHighlights(); 8616 financeEligibilityScrollToFirstInvalid(getEmploymentUiHost()); 8617 return; 8618 } 8619 const entry = buildEmploymentEntryFromForm(editingEmploymentId != null ? editingEmploymentId : undefined); 8620 if (editingEmploymentId != null && editingEmploymentIndex != null) { 8621 savedEmploymentEntries.splice(editingEmploymentIndex, 1, entry); 8622 } else { 8623 savedEmploymentEntries.push(entry); 8624 } 8625 editingEmploymentId = null; 8626 editingEmploymentIndex = null; 8627 employmentFormMode = 'hidden'; 8628 clearEmploymentDetailFormFields(); 8629 getEmploymentUiHost()?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 8630 const employmentStatusHidden = document.getElementById('cars-fe-employment-status'); 8631 if (employmentStatusHidden) { 8632 employmentStatusHidden.value = ''; 8633 } 8634 syncEmploymentSummaryUiState(); 8635 const okAfterSave = 8636 feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped' 8637 ? validateIncomeOutgoingsGrouped() 8638 : validateEmployment(); 8639 // Only go back to review once the screen is valid, otherwise a short employment history 8640 // would leave the step before its alert could be read (address saves gate the same way). 8641 if (reviewEditReturnPending && okAfterSave) { 8642 showReviewStep(); 8643 } 8644 }); 8645 8646 document.getElementById('cars-fe-employment-add-another-button')?.addEventListener('click', (e) => { 8647 e.preventDefault(); 8648 if (savedEmploymentEntries.length < 1) { 8649 return; 8650 } 8651 employmentFormMode = 'editing'; 8652 editingEmploymentId = null; 8653 editingEmploymentIndex = null; 8654 clearEmploymentDetailFormFields(); 8655 getEmploymentUiHost()?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected')); 8656 const employmentStatusHidden = document.getElementById('cars-fe-employment-status'); 8657 if (employmentStatusHidden) { 8658 employmentStatusHidden.value = ''; 8659 } 8660 syncEmploymentSummaryUiState(); 8661 if (feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped') { 8662 validateIncomeOutgoingsGrouped(); 8663 } else { 8664 validateEmployment(); 8665 } 8666 scrollToTop(getEmploymentUiHost()); 8667 }); 8668 8669 personalGroupedStep?.addEventListener('input', (e) => { 8670 if (personalGroupedStep.hidden || !e.target.matches('input')) { 8671 return; 8672 } 8673 if (e.target.id === 'cars-fe-first-name' || e.target.id === 'cars-fe-last-name') {
8674 stripDigitsFromPersonNameInput(e.target); 8675 } 8676 if (e.target.id === 'cars-fe-phone-national') { 8677 syncLivePhoneFormatFeedback(); 8678 } 8679 if (e.target.id === 'cars-fe-email' || e.target.id === 'cars-fe-dob') { 8680 feFieldBeingEdited = e.target.id; 8681 } 8682 validatePersonalGrouped(); 8683 }); 8684 8685 personalGroupedStep?.addEventListener('change', (e) => { 8686 if (personalGroupedStep.hidden || !e.target.matches('input')) { 8687 return; 8688 } 8689 validatePersonalGrouped(); 8690 }); 8691 8692 personalGroupedStep?.addEventListener('click', (e) => { 8693 const pill = e.target.closest('.cars-fe-pill'); 8694 if (!pill || !personalGroupedStep.contains(pill)) { 8695 return; 8696 } 8697 const group = pill.closest('.cars-fe-pills'); 8698 if (!group) { 8699 return; 8700 } 8701 group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected')); 8702 pill.classList.add('is-selected'); 8703 const hidden = group.querySelector('input[type="hidden"]'); 8704 if (hidden) { 8705 hidden.value = pill.dataset.value || ''; 8706 } 8707 validatePersonalGrouped(); 8708 }); 8709 8710 detailsStep?.addEventListener('input', (e) => { 8711 if (!detailsStep.hidden && e.target.matches('input')) { 8712 if (e.target.id === 'cars-fe-first-name' || e.target.id === 'cars-fe-last-name') { 8713 stripDigitsFromPersonNameInput(e.target); 8714 } 8715 if (e.target.id === 'cars-fe-dob') { 8716 feFieldBeingEdited = e.target.id; 8717 } 8718 validateDetails(); 8719 } 8720 }); 8721 8722 detailsStep?.addEventListener('change', (e) => { 8723 if (!detailsStep.hidden && e.target.matches('input')) { 8724 validateDetails(); 8725 } 8726 }); 8727 8728 contactStep?.addEventListener('input', (e) => { 8729 if (!contactStep.hidden && e.target.matches('input')) { 8730 if (e.target.id === 'cars-fe-phone-national') { 8731 syncLivePhoneFormatFeedback(); 8732 } 8733 if (e.target.id === 'cars-fe-email') { 8734 feFieldBeingEdited = e.target.id; 8735 } 8736 validateContact(); 8737 } 8738 }); 8739 8740 detailsStep?.addEventListener('click', (e) => { 8741 const pill = e.target.closest('.cars-fe-pill'); 8742 if (!pill || !detailsStep.contains(pill)) { 8743 return; 8744 } 8745 const group = pill.closest('.cars-fe-pills'); 8746 if (!group) { 8747 return; 8748 } 8749 group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected')); 8750 pill.classList.add('is-selected'); 8751 const hidden = group.querySelector('input[type="hidden"]'); 8752 if (hidden) { 8753 hidden.value = pill.dataset.value || ''; 8754 } 8755 validateDetails(); 8756 }); 8757 8758 /** Re-runs the validator owning `el`, so leaving a field can surface just that field's error. */ 8759 const revalidateFeStepContaining = (el) => { 8760 const steps = [ 8761 [personalGroupedStep, validatePersonalGrouped], 8762 [detailsStep, validateDetails], 8763 [contactStep, validateContact], 8764 [moreStep, validateMore], 8765 [addressStep, validateAddress], 8766 [incomeOutgoingsGroupedStep, validateIncomeOutgoingsGrouped], 8767 [getEmploymentUiHost(), validateEmployment], 8768 [incomeOutgoingsStep, validateIncomeOutgoings], 8769 [outgoingsFinalStep, validateOutgoingsFinal], 8770 ]; 8771 for (const [stepEl, validate] of steps) { 8772 if (stepEl && !stepEl.hidden && stepEl.contains(el)) { 8773 validate(); 8774 return; 8775 } 8776 } 8777 }; 8778 8779 // Leaving a field marks it touched so its error shows before Next is pressed. 8780 panel.addEventListener('focusout', (e) => { 8781 const field = e.target; 8782 if (!field || !field.id || !field.matches?.('input, select, textarea')) { 8783 return; 8784 } 8785 // A tracked field's error is held back while it is being edited, so leaving it has to 8786 // revalidate even when the field was already marked touched earlier in the journey. 8787 const leftFieldBeingEdited = feFieldBeingEdited === field.id; 8788 if (leftFieldBeingEdited) { 8789 feFieldBeingEdited = null; 8790 } 8791 if (!feMarkFieldTouched(field.id) && !leftFieldBeingEdited) { 8792 return; 8793 } 8794 revalidateFeStepContaining(field); 8795 }); 8796 8797 addressStep?.addEventListener('input', (e) => { 8798 if (addressStep.hidden || !e.target.matches('input, select')) { 8799 return; 8800 } 8801 // Postcoder owns autocomplete for #cars-fe-address-postcode â skip legacy select lookup. 8802 if ( 8803 e.target.id === 'cars-fe-address-postcode' && 8804 typeof PostcoderAutocomplete !== 'function' 8805 ) { 8806 clearTimeout(addressLookupTimer); 8807 addressLookupTimer = setTimeout(runAddressLookup, 400); 8808 } 8809 if (e.target.id === 'cars-fe-address-postcode-manual') { 8810 syncLiveManualUkPostcodeFeedback(); 8811 } 8812 validateAddress(); 8813 }); 8814 8815 addressStep?.addEventListener('change', (e) => { 8816 if (addressStep.hidden || !addressStep.contains(e.target)) { 8817 return; 8818 } 8819 if (e.target.id === 'cars-fe-address-select') { 8820 openManualCardForLookupSelection(e.target.value); 8821 } 8822 validateAddress(); 8823 }); 8824 8825 moreStep?.addEventListener('click', (e) => { 8826 const pill = e.target.closest('.cars-fe-pill'); 8827 if (!pill || !moreStep.contains(pill)) { 8828 return; 8829 } 8830 const group = pill.closest('.cars-fe-pills'); 8831 if (!group) { 8832 return; 8833 } 8834 group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected')); 8835 pill.classList.add('is-selected'); 8836 const hidden = group.querySelector('input[type="hidden"]'); 8837 if (hidden) { 8838 hidden.value = pill.dataset.value || ''; 8839 } 8840 validateMore(); 8841 }); 8842 8843 employmentStep?.addEventListener('click', (e) => { 8844 if (!e.target.closest('.cars-fe-occupation-combobox')) { 8845 financeEligibilityCloseOccupationPanels(employmentStep); 8846 } 8847 if (!e.target.closest('.cars-fe-duration-listbox')) { 8848 financeEligibilityCloseDurationListboxes(employmentStep); 8849 } 8850 const editBtn = e.target.closest('[data-cars-fe-employment-edit]'); 8851 if (editBtn && employmentStep.contains(editBtn)) { 8852 const entryId = editBtn.getAttribute('data-cars-fe-employment-edit'); 8853 const idx = savedEmploymentEntries.findIndex((x) => x.id === entryId); 8854 if (idx < 0) { 8855 return; 8856 } 8857 const entry = savedEmploymentEntries[idx]; 8858 employmentFormMode = 'editing'; 8859 editingEmploymentId = entry.id; 8860 editingEmploymentIndex = idx; 8861 applyEmploymentEntryToForm(entry); 8862 syncEmploymentSummaryUiState(); 8863 validateEmployment(); 8864 scrollToTop(employmentStep); 8865 return; 8866 } 8867 const pill = e.target.closest('.cars-fe-pill'); 8868 if (!pill || !employmentStep.contains(pill)) { 8869 return; 8870 } 8871 const group = pill.closest('.cars-fe-pills'); 8872 if (!group) { 8873 return; 8874 } 8875 if (!group.classList.contains('cars-fe-pills--employment')) { 8876 return; 8877 } 8878 group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected')); 8879 pill.classList.add('is-selected'); 8880 const hidden = group.querySelector('input[type="hidden"]'); 8881 const pillVal = pill.getAttribute('data-value') || pill.dataset.value || ''; 8882 if (hidden) { 8883 hidden.value = pillVal;
8884 } 8885 applyEmploymentStatusAutofill(pillVal); 8886 syncEmploymentDurationLabelForStatus(pillVal); 8887 validateEmployment(); 8888 }); 8889 8890 const handleIncomeOutgoingsGroupedClick = (e) => { 8891 const host = incomeOutgoingsGroupedStep; 8892 if (!host || host.hidden) { 8893 return; 8894 } 8895 if (!e.target.closest('.cars-fe-occupation-combobox')) { 8896 financeEligibilityCloseOccupationPanels(host); 8897 } 8898 if (!e.target.closest('.cars-fe-duration-listbox')) { 8899 financeEligibilityCloseDurationListboxes(host); 8900 } 8901 const editBtn = e.target.closest('[data-cars-fe-employment-edit]'); 8902 if (editBtn && host.contains(editBtn)) { 8903 const entryId = editBtn.getAttribute('data-cars-fe-employment-edit'); 8904 const idx = savedEmploymentEntries.findIndex((x) => x.id === entryId); 8905 if (idx < 0) { 8906 return; 8907 } 8908 const entry = savedEmploymentEntries[idx]; 8909 employmentFormMode = 'editing'; 8910 editingEmploymentId = entry.id; 8911 editingEmploymentIndex = idx; 8912 applyEmploymentEntryToForm(entry); 8913 syncEmploymentSummaryUiState(); 8914 validateIncomeOutgoingsGrouped(); 8915 scrollToTop(host); 8916 return; 8917 } 8918 const pill = e.target.closest('.cars-fe-pill'); 8919 if (!pill || !host.contains(pill)) { 8920 return; 8921 } 8922 const group = pill.closest('.cars-fe-pills'); 8923 if (!group) { 8924 return; 8925 } 8926 group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected')); 8927 pill.classList.add('is-selected'); 8928 const hidden = group.querySelector('input[type="hidden"]'); 8929 const pillVal = pill.getAttribute('data-value') || pill.dataset.value || ''; 8930 if (hidden) { 8931 hidden.value = pillVal; 8932 } 8933 if (group.classList.contains('cars-fe-pills--employment')) { 8934 applyEmploymentStatusAutofill(pillVal); 8935 syncEmploymentDurationLabelForStatus(pillVal); 8936 } 8937 syncOutgoingsConditionalVisibility(); 8938 validateIncomeOutgoingsGrouped(); 8939 }; 8940 8941 incomeOutgoingsGroupedStep?.addEventListener('click', handleIncomeOutgoingsGroupedClick); 8942 8943 incomeOutgoingsGroupedStep?.addEventListener('input', (e) => { 8944 if (incomeOutgoingsGroupedStep.hidden || !incomeOutgoingsGroupedStep.contains(e.target)) { 8945 return; 8946 } 8947 if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) { 8948 applySterlingThousandsFormatting(e.target); 8949 } 8950 if (e.target.matches('input, select')) { 8951 syncOutgoingsConditionalVisibility(); 8952 validateIncomeOutgoingsGrouped(); 8953 } 8954 }); 8955 8956 incomeOutgoingsGroupedStep?.addEventListener('change', (e) => { 8957 if (incomeOutgoingsGroupedStep.hidden || !incomeOutgoingsGroupedStep.contains(e.target)) { 8958 return; 8959 } 8960 if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) { 8961 applySterlingThousandsFormatting(e.target); 8962 } 8963 syncOutgoingsConditionalVisibility(); 8964 validateIncomeOutgoingsGrouped(); 8965 }); 8966 8967 employmentStep?.addEventListener('input', (e) => { 8968 if (employmentStep.hidden || !employmentStep.contains(e.target)) { 8969 return; 8970 } 8971 if (e.target.matches('input, select')) { 8972 validateEmployment(); 8973 } 8974 }); 8975 8976 employmentStep?.addEventListener('change', (e) => { 8977 if (employmentStep.hidden || !employmentStep.contains(e.target)) { 8978 return; 8979 } 8980 validateEmployment(); 8981 }); 8982 8983 incomeOutgoingsStep?.addEventListener('input', (e) => { 8984 if (incomeOutgoingsStep.hidden || !incomeOutgoingsStep.contains(e.target)) { 8985 return; 8986 } 8987 if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) { 8988 applySterlingThousandsFormatting(e.target); 8989 } 8990 if (e.target.matches('input')) { 8991 validateIncomeOutgoings(); 8992 } 8993 }); 8994 8995 incomeOutgoingsStep?.addEventListener('change', (e) => { 8996 if (incomeOutgoingsStep.hidden || !incomeOutgoingsStep.contains(e.target)) { 8997 return; 8998 } 8999 if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) { 9000 applySterlingThousandsFormatting(e.target); 9001 } 9002 validateIncomeOutgoings(); 9003 }); 9004 9005 outgoingsFinalStep?.addEventListener('input', (e) => { 9006 if (outgoingsFinalStep.hidden || !outgoingsFinalStep.contains(e.target)) { 9007 return; 9008 } 9009 if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) { 9010 applySterlingThousandsFormatting(e.target); 9011 } 9012 if (e.target.matches('input')) { 9013 validateOutgoingsFinal(); 9014 } 9015 }); 9016 9017 outgoingsFinalStep?.addEventListener('change', (e) => { 9018 if (outgoingsFinalStep.hidden || !outgoingsFinalStep.contains(e.target)) { 9019 return; 9020 } 9021 if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) { 9022 applySterlingThousandsFormatting(e.target); 9023 } 9024 validateOutgoingsFinal(); 9025 }); 9026 9027 outgoingsFinalStep?.addEventListener('click', (e) => { 9028 const pill = e.target.closest('.cars-fe-pill'); 9029 if (!pill || !outgoingsFinalStep.contains(pill)) { 9030 return; 9031 } 9032 const group = pill.closest('.cars-fe-pills'); 9033 if (!group) { 9034 return; 9035 } 9036 group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected')); 9037 pill.classList.add('is-selected'); 9038 const hidden = group.querySelector('input[type="hidden"]'); 9039 if (hidden) { 9040 hidden.value = pill.dataset.value || ''; 9041 } 9042 syncOutgoingsConditionalVisibility(); 9043 if (val('cars-fe-outgoings-repay-existing-finance') !== 'Yes') { 9044 clearOutgoingsRepayFollowupFields(); 9045 } 9046 if (val('cars-fe-outgoings-downturn-expected') !== 'Yes') { 9047 clearOutgoingsDownturnFollowupFields(); 9048 } 9049 validateOutgoingsFinal(); 9050 }); 9051 9052 panel.addEventListener('click', (e) => { 9053 if (!e.target.closest('.cars-fe-duration-listbox')) { 9054 financeEligibilityCloseDurationListboxes(); 9055 } 9056 }); 9057 9058 panel.addEventListener('focusin', (e) => { 9059 const t = e.target; 9060 if (!t || !panel.contains(t)) { 9061 return; 9062 } 9063 // Duration open already scrolls once; skip focusin to avoid flicker. 9064 if (t.matches('.cars-fe-duration-listbox__trigger')) { 9065 return; 9066 } 9067 if (t.matches('input[type="hidden"], button, .cars-fe-pill, .cars-fe-footer__back')) { 9068 return; 9069 } 9070 // Postcode suggestions handle their own scroll when the list opens. 9071 if (t.id === 'cars-fe-address-postcode') { 9072 return; 9073 } 9074 if ( 9075 t.matches( 9076 'select, .cars-fe-occupation-combobox__input, .cars-fe-field-group__input--manual-select, .cars-fe-field-group__input--address-select, #cars-fe-address-residency', 9077 ) 9078 ) { 9079 financeEligibilityScheduleFieldScrollIntoView(t); 9080 } 9081 }); 9082 9083 panel.addEventListener('change', (e) => { 9084 const t = e.target; 9085 if (t && t.closest && t.closest('.cars-fe-field-group__shell--duration')) { 9086 syncFinanceDurationSelectShell(t); 9087 } 9088 }); 9089 9090 panel.addEventListener('show.bs.offcanvas', () => { 9091 syncFinanceEligibilityPanelIvendiFromVdp(); 9092 syncFinanceEligibilityListingFromPanel(); 9093 syncEligibilityAgreementRepresentative(); 9094 9095 financeEligibilityLockedScrollY = window.scrollY || window.pageYOffset || 0; 9096 document.documentElement.classList.add('finance-eligibility-offcanvas-open'); 9097 const body = document.body; 9098 body.style.position = 'fixed'; 9099 body.style.top = `-${financeEligibilityLockedScrollY}px`;
9100 body.style.left = '0'; 9101 body.style.right = '0'; 9102 body.style.width = '100%'; 9103 9104 if (financeEligibilityViewportLockCleanup) { 9105 financeEligibilityViewportLockCleanup(); 9106 financeEligibilityViewportLockCleanup = null; 9107 } 9108 9109 const feScrollSelector = [ 9110 '.cars-fe-scroll', 9111 '.finance-eligibility-intro__scroll', 9112 '.finance-eligibility-agreement__scroll', 9113 '.cars-fe-results-modal__scroll', 9114 '.cars-fe-lender-detail__scroll', 9115 '.cars-fe-results-glossary__scroll', 9116 '.cars-fe-occupation-combobox__list', 9117 '.cars-fe-address-suggestions', 9118 '.cars-fe-duration-listbox', 9119 ].join(', '); 9120 9121 const findFeScrollParent = (target) => { 9122 if (!target || typeof target.closest !== 'function') { 9123 return null; 9124 } 9125 return target.closest(feScrollSelector); 9126 }; 9127 9128 let feTouchLastY = 0; 9129 const onTouchStartWhileOpen = (e) => { 9130 feTouchLastY = e.touches && e.touches[0] ? e.touches[0].clientY : 0; 9131 }; 9132 9133 const onTouchMoveWhileOpen = (e) => { 9134 if (!panel.classList.contains('show')) { 9135 return; 9136 } 9137 const scrollEl = findFeScrollParent(e.target); 9138 if (!scrollEl) { 9139 if (e.cancelable) { 9140 e.preventDefault(); 9141 } 9142 return; 9143 } 9144 const y = e.touches && e.touches[0] ? e.touches[0].clientY : feTouchLastY; 9145 const deltaY = y - feTouchLastY; 9146 feTouchLastY = y; 9147 const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight; 9148 if (maxScroll <= 1) { 9149 if (e.cancelable) { 9150 e.preventDefault(); 9151 } 9152 return; 9153 } 9154 const atTop = scrollEl.scrollTop <= 0; 9155 const atBottom = scrollEl.scrollTop >= maxScroll - 1; 9156 if ((atTop && deltaY > 0) || (atBottom && deltaY < 0)) { 9157 if (e.cancelable) { 9158 e.preventDefault(); 9159 } 9160 } 9161 }; 9162 9163 document.addEventListener('touchstart', onTouchStartWhileOpen, { passive: true, capture: true }); 9164 document.addEventListener('touchmove', onTouchMoveWhileOpen, { passive: false, capture: true }); 9165 9166 const vv = window.visualViewport; 9167 const syncKeyboardOverlayClass = () => { 9168 if (!panel.classList.contains('show')) { 9169 return; 9170 } 9171 panel.style.removeProperty('height'); 9172 panel.style.removeProperty('max-height'); 9173 panel.style.removeProperty('min-height'); 9174 panel.style.top = '0px'; 9175 panel.style.bottom = '0px'; 9176 if (vv) { 9177 const h = Math.max(1, Math.round(vv.height)); 9178 const layoutH = window.innerHeight || h; 9179 panel.classList.toggle('cars-fe-keyboard-open', layoutH - h > 150); 9180 } 9181 }; 9182 syncKeyboardOverlayClass(); 9183 if (vv) { 9184 vv.addEventListener('resize', syncKeyboardOverlayClass); 9185 } 9186 9187 financeEligibilityViewportLockCleanup = () => { 9188 document.removeEventListener('touchstart', onTouchStartWhileOpen, true); 9189 document.removeEventListener('touchmove', onTouchMoveWhileOpen, true); 9190 if (vv) { 9191 vv.removeEventListener('resize', syncKeyboardOverlayClass); 9192 } 9193 panel.style.removeProperty('height'); 9194 panel.style.removeProperty('max-height'); 9195 panel.style.removeProperty('min-height'); 9196 panel.style.removeProperty('top'); 9197 panel.style.removeProperty('bottom'); 9198 panel.classList.remove('cars-fe-keyboard-open'); 9199 body.style.removeProperty('position'); 9200 body.style.removeProperty('top'); 9201 body.style.removeProperty('left'); 9202 body.style.removeProperty('right'); 9203 body.style.removeProperty('width'); 9204 window.scrollTo(0, financeEligibilityLockedScrollY || 0); 9205 financeEligibilityLockedScrollY = 0; 9206 }; 9207 }); 9208 9209 panel.addEventListener('shown.bs.offcanvas', () => { 9210 applyFormDensityVariant(); 9211 syncAllFinanceDurationSelectShells(); 9212 if (agreementView && agreementView.hidden === false) { 9213 syncEligibilityAgreementRepresentative(); 9214 } 9215 if (personalGroupedStep && personalGroupedStep.hidden === false) { 9216 applyFinanceEligibilityUkDial(); 9217 validatePersonalGrouped(); 9218 } 9219 if (detailsStep && detailsStep.hidden === false) { 9220 validateDetails(); 9221 } 9222 if (contactStep && contactStep.hidden === false) { 9223 applyFinanceEligibilityUkDial(); 9224 validateContact(); 9225 } 9226 if (moreStep && moreStep.hidden === false) { 9227 validateMore(); 9228 } 9229 if (addressStep && addressStep.hidden === false) { 9230 validateAddress(); 9231 } 9232 if (incomeOutgoingsGroupedStep && incomeOutgoingsGroupedStep.hidden === false) { 9233 financeEligibilityCloseOccupationPanels(incomeOutgoingsGroupedStep); 9234 syncEmploymentSummaryUiState(); 9235 validateIncomeOutgoingsGrouped(); 9236 } 9237 if (employmentStep && employmentStep.hidden === false) { 9238 financeEligibilityCloseOccupationPanels(employmentStep); 9239 syncEmploymentSummaryUiState(); 9240 validateEmployment(); 9241 } 9242 if (incomeOutgoingsStep && incomeOutgoingsStep.hidden === false) { 9243 validateIncomeOutgoings(); 9244 } 9245 if (outgoingsFinalStep && outgoingsFinalStep.hidden === false) { 9246 validateOutgoingsFinal(); 9247 } 9248 if (reviewStep && reviewStep.hidden === false) { 9249 validateReviewConsent(); 9250 } 9251 syncFinanceEligibilityJourneyUrl(); 9252 }); 9253
9254 document.addEventListener('big-finance-quotes-updated', () => { 9255 syncFinanceEligibilityPanelIvendiFromVdp(); 9256 syncEligibilityAgreementRepresentative(); 9257 syncReviewConsentLenderName(); 9258 }); 9259 9260 panel.addEventListener('hidden.bs.offcanvas', () => { 9261 clearFinanceEligibilityJourneyUrl(); 9262 panel.removeAttribute('data-fe-bound-listing-cta'); 9263 // Listing-CTA vehicle details are per-card, so never let them carry over to the next open. 9264 panel.removeAttribute('data-fe-stock-image'); 9265 panel.removeAttribute('data-fe-stock-price'); 9266 panel.removeAttribute('data-fe-stock-mileage'); 9267 if (financeEligibilityViewportLockCleanup) { 9268 financeEligibilityViewportLockCleanup(); 9269 financeEligibilityViewportLockCleanup = null; 9270 } 9271 document.documentElement.classList.remove('finance-eligibility-offcanvas-open'); 9272 if (intro) { 9273 intro.hidden = false; 9274 } 9275 if (agreementView) { 9276 agreementView.hidden = true; 9277 } 9278 if (detailsStep) { 9279 detailsStep.hidden = true; 9280 } 9281 hideContactStep(); 9282 hideMoreStep(); 9283 hidePersonalGroupedStep(); 9284 hideAddressStep(); 9285 hideEmploymentStep(); 9286 hideIncomeOutgoingsStep(); 9287 hideOutgoingsFinalStep(); 9288 hideIncomeOutgoingsGroupedStep(); 9289 hideReviewStep(); 9290 restoreControlFormDomIfComposed(); 9291 if (panel) { 9292 panel.dataset.feFormDensity = isGroupedFormActive() ? 'grouped' : 'control'; 9293 } 9294 resetJourneyForms(); 9295 state.screen = 'intro'; 9296 state.dirty = false; 9297 // Re-apply after reset so intro chrome matches the current flag for the next open. 9298 applyFormDensityVariant(); 9299 }); 9300 9301 panel.addEventListener('hide.bs.offcanvas', (e) => { 9302 if ( 9303 state.dirty && 9304 (state.screen === 'details' || 9305 state.screen === 'contact' || 9306 state.screen === 'more' || 9307 state.screen === 'personalGrouped' || 9308 state.screen === 'address' || 9309 state.screen === 'employment' || 9310 state.screen === 'incomeOutgoings' || 9311 state.screen === 'outgoingsFinal' || 9312 state.screen === 'incomeOutgoingsGrouped' || 9313 state.screen === 'review') 9314 ) { 9315 if (!window.confirm('You have unsaved information. Close and lose progress?')) { 9316 e.preventDefault(); 9317 } 9318 } 9319 }); 9320 9321 setBtnNextDisabled(nextBtn, true); 9322 setBtnNextDisabled(contactNextBtn, true); 9323 setBtnNextDisabled(moreNextBtn, true); 9324 setBtnNextDisabled(personalGroupedNextBtn, true); 9325 setBtnNextDisabled(addressNextBtn, true); 9326 setBtnNextDisabled(employmentNextBtn, true); 9327 setBtnNextDisabled(incomeNextBtn, true); 9328 setBtnNextDisabled(outgoingsSummaryBtn, true); 9329 setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, true); 9330 setBtnNextDisabled(reviewViewResultsBtn, true); 9331} 9332 9333(function initFinanceEligibilityPanelWhenReady() { 9334 if (document.readyState === 'loading') { 9335 document.addEventListener('DOMContentLoaded', setupCarsFinanceEligibilityPanel); 9336 } else { 9337 setupCarsFinanceEligibilityPanel(); 9338 } 9339})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.