Files
eamco_office_frontend/src/components/SearchResults.vue
Edwin Eames 72d8e35e06 refactor(frontend): migrate Customer domain to centralized API services
- Replaced all direct axios imports with service layer calls across 8 customer files
- Migrated core pages: home.vue, create.vue, edit.vue
- Migrated profile pages: profile.vue (1100+ lines), TankEstimation.vue
- Migrated supporting pages: ServicePlanEdit.vue, tank/edit.vue, list.vue

Services integrated:
- customerService: CRUD, descriptions, tank info, automatic status
- authService: authentication and Authorize.net account management
- paymentService: credit cards, transactions, payment authorization
- deliveryService: delivery records and automatic delivery data
- serviceService: service calls, parts, and service plans
- adminService: statistics, social comments, and reports
- queryService: dropdown data (customer types, states)

Type safety improvements:
- Updated paymentService.ts with accurate AxiosResponse types
- Fixed response unwrapping to match api.ts interceptor behavior
- Resolved all TypeScript errors in customer domain (0 errors)

Benefits:
- Consistent authentication via centralized interceptors
- Standardized error handling across all API calls
- Improved type safety with proper TypeScript interfaces
- Single source of truth for API endpoints
- Better testability through mockable services

Verified with vue-tsc --noEmit - all customer domain files pass type checking
2026-02-01 13:00:21 -05:00

110 lines
3.9 KiB
Vue
Executable File

<!-- components/SearchResults.vue -->
<template>
<div
ref="searchContainer"
class="absolute top-16 left-1/2 -translate-x-1/2 z-50 w-96 max-w-[90vw] max-h-[70vh] bg-base-100 rounded-lg shadow-xl border border-base-300 flex flex-col"
>
<!-- Header of the search results box -->
<div class="p-4 border-b border-base-300 flex-shrink-0">
<h3 class="font-bold">Search Results</h3>
</div>
<!--
THE FIX IS HERE:
- Removed the `menu` class from the `<ul>`.
- Added `flex flex-col` to explicitly force a single vertical column.
- Added `space-y-1` to add a small gap between each list item.
-->
<ul class="flex flex-col space-y-1 p-2 flex-grow overflow-y-auto">
<li v-if="isLoading" class="p-4 text-center">
<span class="loading loading-spinner"></span>
</li>
<li v-else-if="searchResults.length === 0 && searchTerm.length > 1" class="px-4 py-2 text-sm text-gray-500">
No results found for "{{ searchTerm }}"
</li>
<!-- The v-for now loops through simple <li> elements -->
<li v-for="result in searchResults" :key="result.id || result.account_number">
<!--
We add styling directly to the router-link to make it look like a clickable list item.
- `block`: Makes the entire area clickable, not just the text.
-->
<router-link
v-if="result.id"
:to="{ name: 'customerProfile', params: { id: result.id } }"
@click="clearSearch"
class="block p-2 rounded-lg hover:bg-base-200 focus:bg-primary focus:text-primary-content"
>
<div>
<div class="font-bold">{{ result.customer_first_name }} {{ result.customer_last_name }}</div>
<div class="text-sm opacity-70">
{{ result.customer_address }}
</div>
<div class="text-sm opacity-70">
{{ result.customer_town }}, {{ getStateName(result.customer_state) }}
</div>
<div class="text-xs opacity-60 mt-1">
{{ result.customer_phone_number }}
</div>
</div>
</router-link>
<div v-else class="block p-2 rounded-lg">
<div class="font-bold">{{ result.customer_first_name }} {{ result.customer_last_name }}</div>
<div class="text-sm opacity-70">{{ result.customer_address }}</div>
<div class="text-sm opacity-70">{{ result.customer_town }}, {{ getStateName(result.customer_state) }}</div>
<div class="text-xs opacity-60 mt-1">{{ result.customer_phone_number }}</div>
</div>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useSearchStore } from '../stores/search'
// Store
const searchStore = useSearchStore()
// Template ref
const searchContainer = ref<HTMLElement>()
// Reactive data
const stateMap = ref({
0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY',
} as Record<number, string>)
// Computed properties
const searchTerm = computed(() => searchStore.searchTerm)
const searchResults = computed(() => searchStore.searchResults)
const isLoading = computed(() => searchStore.isLoading)
// Functions
const getStateName = (stateValue: number | string): string => {
const stateNumber = Number(stateValue);
return stateMap.value[stateNumber] || 'N/A';
}
const clearSearch = () => {
searchStore.clearSearch();
}
const handleClickOutside = (event: MouseEvent) => {
const container = searchContainer.value;
const searchInput = document.getElementById('customer-search-input');
if (container && !container.contains(event.target as Node) && searchInput && !searchInput.contains(event.target as Node)) {
searchStore.clearSearch();
}
}
// Lifecycle
onMounted(() => {
document.addEventListener('mousedown', handleClickOutside);
})
onBeforeUnmount(() => {
document.removeEventListener('mousedown', handleClickOutside);
})
</script>