feat(ui): Massive frontend modernization including customer table redesign, new map features, and consistent styling

This commit is contained in:
2026-02-06 20:31:16 -05:00
parent 421ba896a0
commit 6c28c0c2d2
68 changed files with 7472 additions and 1253 deletions

View File

@@ -0,0 +1,190 @@
import { ref, watch } from 'vue';
import { addressService, TownSuggestion, StreetSuggestion } from '../services/addressService';
/**
* Debounce utility function
*/
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeout: ReturnType<typeof setTimeout>;
return ((...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
}) as T;
}
/**
* Composable for town autocomplete functionality
*/
export function useTownAutocomplete() {
const townSuggestions = ref<TownSuggestion[]>([]);
const isLoadingTowns = ref(false);
const showTownDropdown = ref(false);
const highlightedTownIndex = ref(-1);
const townError = ref('');
const searchTowns = debounce(async (query: string) => {
if (!query || query.length < 2) {
townSuggestions.value = [];
showTownDropdown.value = false;
return;
}
isLoadingTowns.value = true;
townError.value = '';
try {
const response = await addressService.searchTowns(query);
if (response.data.ok) {
townSuggestions.value = response.data.suggestions;
showTownDropdown.value = townSuggestions.value.length > 0;
highlightedTownIndex.value = -1;
}
} catch (error) {
console.error('Town search error:', error);
townError.value = 'Unable to search towns';
townSuggestions.value = [];
} finally {
isLoadingTowns.value = false;
}
}, 300);
const closeTownDropdown = () => {
// Delay to allow click events to fire
setTimeout(() => {
showTownDropdown.value = false;
highlightedTownIndex.value = -1;
}, 150);
};
const handleTownKeydown = (
event: KeyboardEvent,
onSelect: (suggestion: TownSuggestion) => void
) => {
if (!showTownDropdown.value || townSuggestions.value.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
highlightedTownIndex.value = Math.min(
highlightedTownIndex.value + 1,
townSuggestions.value.length - 1
);
break;
case 'ArrowUp':
event.preventDefault();
highlightedTownIndex.value = Math.max(highlightedTownIndex.value - 1, 0);
break;
case 'Enter':
event.preventDefault();
if (highlightedTownIndex.value >= 0) {
onSelect(townSuggestions.value[highlightedTownIndex.value]);
} else if (townSuggestions.value.length === 1) {
onSelect(townSuggestions.value[0]);
}
break;
case 'Escape':
showTownDropdown.value = false;
highlightedTownIndex.value = -1;
break;
}
};
return {
townSuggestions,
isLoadingTowns,
showTownDropdown,
highlightedTownIndex,
townError,
searchTowns,
closeTownDropdown,
handleTownKeydown,
};
}
/**
* Composable for street/address autocomplete functionality
*/
export function useStreetAutocomplete() {
const streetSuggestions = ref<StreetSuggestion[]>([]);
const isLoadingStreets = ref(false);
const showStreetDropdown = ref(false);
const highlightedStreetIndex = ref(-1);
const streetError = ref('');
const searchStreets = debounce(async (town: string, state: string, query: string) => {
if (!town || !state || !query || query.length < 1) {
streetSuggestions.value = [];
showStreetDropdown.value = false;
return;
}
isLoadingStreets.value = true;
streetError.value = '';
try {
const response = await addressService.searchStreets(town, state, query);
if (response.data.ok) {
streetSuggestions.value = response.data.suggestions;
showStreetDropdown.value = streetSuggestions.value.length > 0;
highlightedStreetIndex.value = -1;
}
} catch (error) {
console.error('Street search error:', error);
streetError.value = 'Unable to search addresses';
streetSuggestions.value = [];
} finally {
isLoadingStreets.value = false;
}
}, 300);
const closeStreetDropdown = () => {
setTimeout(() => {
showStreetDropdown.value = false;
highlightedStreetIndex.value = -1;
}, 150);
};
const handleStreetKeydown = (
event: KeyboardEvent,
onSelect: (suggestion: StreetSuggestion) => void
) => {
if (!showStreetDropdown.value || streetSuggestions.value.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
highlightedStreetIndex.value = Math.min(
highlightedStreetIndex.value + 1,
streetSuggestions.value.length - 1
);
break;
case 'ArrowUp':
event.preventDefault();
highlightedStreetIndex.value = Math.max(highlightedStreetIndex.value - 1, 0);
break;
case 'Enter':
event.preventDefault();
if (highlightedStreetIndex.value >= 0) {
onSelect(streetSuggestions.value[highlightedStreetIndex.value]);
} else if (streetSuggestions.value.length === 1) {
onSelect(streetSuggestions.value[0]);
}
break;
case 'Escape':
showStreetDropdown.value = false;
highlightedStreetIndex.value = -1;
break;
}
};
return {
streetSuggestions,
isLoadingStreets,
showStreetDropdown,
highlightedStreetIndex,
streetError,
searchStreets,
closeStreetDropdown,
handleStreetKeydown,
};
}