Ad Hoc Data Selectietool

Geographic Search Enhancement for Legacy B2B Data Platform

Visit AdHocData.be
JavaScriptAngular 1.xGeoJSONLESSLeaflet
Loading map...
Geo Zoeken
1.2M+ BE bedrijven
Legacy Refactor
GeoJSON polygons
KBO
Thematic Search
0400+
Search Terms
Geographic Search
01,053
Regions
Code Architecture
033
Modules
Production Ready
90
TODOs
Design System
1740+
Design Tokens
Zero Conflicts
0%100%
Scoped CSS

Overview

Complete UI/UX redesign and refactor of a B2B selection tool for 1.2M+ Belgian companies. The original spaghetti code was hard to maintain - now split into 33 modules. During the project, I discovered that the existing dataset and API endpoints also enabled geographic and thematic search - two features I added as extras.

The Challenge

Modernizing a legacy stack (Angular 1.x, LESS, vanilla JS) with unstructured code into a maintainable system, without changing existing functionality or API structure.

The Solution

Code Refactor

Spaghetti code split into 33 organized modules with clear sections and config objects.

Design System

From 352 to 5,848 lines of CSS with 40+ design tokens, fully scoped to avoid conflicts.

Geo Search

GeoJSON polygons for 1,053 Belgian regions with an interactive Leaflet map interface for visual multi-selection - discovered from existing coordinates in exports.

Thematic Search

400+ search terms that automatically map to NACE codes - built on existing API endpoints.

Thematic Search

Search by industry theme instead of NACE codes

Users no longer need to know exact NACE codes. Simply type 'horeca', 'bakker', or 'advocaat' and the system automatically maps these terms to the correct NACE codes. Supports 400+ search terms in both Dutch and French, including singular and plural forms.

horecaI, 55, 56
bakker10.71, 47.24
advocaat69.10
Branche (NACE-BEL 2025)
|
Selecteer de branche(s) waarop u wilt filteren. U kunt meerdere branches selecteren.
AA - Landbouw, Bosbouw En Visserij
BB - Winning Van Delfstoffen
CC - Industrie
DD - Productie En Distributie Van Elektriciteit, Gas, Stoom En Gekoelde Lucht
EE - Distributie Van Water; Afval- En Afvalwaterbeheer En Sanering
FF - Bouwnijverheid
GG - Groot- En Detailhandel
HH - Vervoer En Opslag
II - Verschaffen Van Accommodatie En Maaltijden
JJ - Informatie En Communicatie
MM - Vrije Beroepen En Wetenschappelijke Activiteiten
QQ - Menselijke Gezondheidszorg En Maatschappelijke Dienstverlening
SS - Overige Diensten
1.200.950bedrijven

UI/UX Redesign

From minimal Bootstrap to a complete custom design system

Modern custom design system
Legacy Bootstrap UI
Before
After
Drag to compare
022
Colors
Complete palette
040+
Design Tokens
Consistent styling
03
Shadow Levels
Depth hierarchy
0%100%
Scoped CSS
Zero conflicts

Color System

22 color tokens: primary, secondary, accent, backgrounds, text, and status colors

Spacing Scale

5 spacing tokens (xs to xl) for consistent margins and padding

Shadow Hierarchy

3 shadow levels (sm, md, lg) for visual depth without harshness

Scoped Styles

All CSS scoped under .ahd-selectietool-modern - zero conflicts with existing styles

Code Refactoring

From spaghetti to structured: a complete architectural overhaul

Controller
4,7265,678
lines
Around same size, now in 33 modules
CSS / LESS
3525,848
lines
Full design system with 40+ tokens
HTML
1,0552,323
lines
Semantic structure & accessibility

Error Handling

⚠️ Haiku poems as error messages

Try/catch with meaningful logging

Legacy
const explanations = {
  noField: `
    expectation void
    a mystery wavering
    surge of heat and cold
  `,
  notAFunction: `
    action required
    laziness or negligence
    action required
  `,
};
Refactored
function saveFilterState() {
  try {
    var state = {
      selectedNaceCodes: vm.selectedNaceCodes || [],
      selectedLocations: vm.selectedLocations || { included: [], excluded: [] },
      filters: {}
    };
    window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
    console.log('[SessionStorage] Filter state saved');
  } catch (e) {
    console.warn('[SessionStorage] Could not save:', e);
  }
}

Code Structure

⚠️ No sections, mixed concerns, jQuery in Angular

Clear sections with config objects

Legacy
Object.assign(window, {
  vars, vm: this,
  $http, $params, $webhooks, $scope, $filter,
  wipe: () => {
    sessionStorage.clear();
    localStorage.clear();
  },
});

$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="collapse"]').on('click', function () {
  var e = $(this);
  setTimeout(function () { /* nested callbacks */ }, 400);
});
Refactored
// ============================================
// API CONFIGURATIE
// ============================================
var API_CONFIG = {
  baseUrl: '/api',
  merchantId: null,
  langKey: 'NL'
};

// ============================================
// LOOKUP ID MAPPING
// ============================================
var LOOKUP_IDS = {
  NACE_2025: { NL: '2025_nace', FR: '2025_nace' },
  WERKKLASSE: { NL: 'NLBE_WERKNEMERSKLASSE', FR: 'FRBE_WERKNEMERSKLASSE' },
  // ...
};

Initialization Flow

⚠️ Nested setTimeout callbacks with magic delays

Clean promise chain with named functions

Legacy
Promise.allSettled(FilterField.lookupRequestList)
.then((values) => {
  setTimeout( () => {
    if (isModelDictUsed) {
      for (let item of fieldContainer.getItems()) {
        // deep nesting continues...
      }
    }
    vm.addressCount.count();
  }, 500); // magic number delay
});
Refactored
function initializeApp() {
  loadLookupData()
    .then(buildFilterTree)
    .then(restoreFilterState)
    .then(updateCount)
    .catch(handleError);
}

function restoreFilterState() {
  var stored = sessionStorage.getItem(STORAGE_KEY);
  if (!stored) return;
  var state = JSON.parse(stored);
  // Clean restore logic
}

CSS Design System

⚠️ Hardcoded values, no scoping, Bootstrap overrides

40+ design tokens, fully scoped styles

Legacy
#pills-tab:before {
  border-left: 1px solid var(--bs-secondary);
  border-top-right-radius: 0.375rem;
}

ul.fancytree-container ul {
  padding: 0 !important;
  padding-left: 1em !important;
}
Refactored
// Design Tokens
@ahd-primary:       #2563eb;
@ahd-border-radius: 10px;
@ahd-shadow-md:     0 4px 6px rgba(0,0,0,0.05);

// All styles scoped
.ahd-selectietool-modern {
  background: @ahd-bg-main;
  border-radius: @ahd-border-radius;
  box-shadow: @ahd-shadow-md;
}

Results

Geographic search: filter 1.2M+ companies across 43 arrondissements, 10 provinces, and 1,000+ postal codes

Thematic search: 400+ search terms to find companies by industry without knowing NACE codes

Code refactored from 0 to 33 organized modules with 0 TODOs remaining

Design system: 352 → 5,848 lines of CSS with 40+ design tokens and 100% scoped styles

Full backward compatibility - zero changes to existing API endpoints