feat(4.1-05-07): create frontend components for spare-parts search integration - useItemSearch hook, LoadingModal, ErrorModal with tests
This commit is contained in:
89
frontend/hooks/useItemSearch.ts
Normal file
89
frontend/hooks/useItemSearch.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export interface SearchResult {
|
||||
category?: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface SearchState {
|
||||
isSearching: boolean;
|
||||
searchError: string | null;
|
||||
searchResults: SearchResult | null;
|
||||
searchStatus: 'idle' | 'searching' | 'success' | 'timeout' | 'error' | 'no_results' | 'skipped';
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
export function useItemSearch(options: { maxRetries?: number; timeout?: number } = {}) {
|
||||
const { maxRetries = 2, timeout = 30000 } = options;
|
||||
|
||||
const [state, setState] = useState<SearchState>({
|
||||
isSearching: false,
|
||||
searchError: null,
|
||||
searchResults: null,
|
||||
searchStatus: 'idle',
|
||||
retryCount: 0,
|
||||
});
|
||||
|
||||
const performSearch = useCallback(
|
||||
async (partNumber: string, category: string): Promise<SearchResult | null> => {
|
||||
if (!partNumber || !category) {
|
||||
setState(prev => ({ ...prev, searchStatus: 'skipped', searchError: null }));
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isSearching: true, searchStatus: 'searching', searchError: null, retryCount: 0 }));
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const response = await fetch('/api/onboarding/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ partNumber, category }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) throw new Error(`Search failed: ${response.status}`);
|
||||
|
||||
const results = await response.json();
|
||||
setState(prev => ({ ...prev, isSearching: false, searchStatus: 'success', searchResults: results }));
|
||||
return results;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Search failed';
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isSearching: false,
|
||||
searchStatus: errorMsg.includes('abort') ? 'timeout' : 'error',
|
||||
searchError: errorMsg,
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[timeout]
|
||||
);
|
||||
|
||||
const retrySearch = useCallback(
|
||||
async (partNumber: string, category: string) => {
|
||||
if (state.retryCount >= maxRetries) {
|
||||
setState(prev => ({ ...prev, searchStatus: 'error' }));
|
||||
return null;
|
||||
}
|
||||
setState(prev => ({ ...prev, retryCount: prev.retryCount + 1 }));
|
||||
return performSearch(partNumber, category);
|
||||
},
|
||||
[state.retryCount, maxRetries, performSearch]
|
||||
);
|
||||
|
||||
const skipSearch = useCallback(() => {
|
||||
setState(prev => ({ ...prev, searchStatus: 'skipped', isSearching: false }));
|
||||
}, []);
|
||||
|
||||
return { state, performSearch, retrySearch, skipSearch };
|
||||
}
|
||||
Reference in New Issue
Block a user