- 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
110 lines
3.9 KiB
Vue
Executable File
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>
|