267 lines
7.7 KiB
TypeScript
267 lines
7.7 KiB
TypeScript
/**
|
|
* Parts lookup utility for frontend
|
|
*
|
|
* Provides functions to fetch parts data by manual filename
|
|
*/
|
|
|
|
import { ebayClient } from './ebay-api'
|
|
|
|
interface PartForPage {
|
|
partNumber: string
|
|
description: string
|
|
ebayListings: Array<{
|
|
itemId: string
|
|
title: string
|
|
price: string
|
|
currency: string
|
|
imageUrl?: string
|
|
viewItemUrl: string
|
|
condition?: string
|
|
shippingCost?: string
|
|
affiliateLink: string
|
|
}>
|
|
}
|
|
|
|
interface ManualPartsLookup {
|
|
[manualFilename: string]: PartForPage[]
|
|
}
|
|
|
|
interface ManualPagesParts {
|
|
[manualFilename: string]: {
|
|
[pageNumber: string]: PartForPage[]
|
|
}
|
|
}
|
|
|
|
// Cache for eBay search results
|
|
const ebaySearchCache = new Map<string, { results: any[]; timestamp: number }>()
|
|
const EBAY_CACHE_TTL = 15 * 60 * 1000 // 15 minutes
|
|
|
|
let manualPartsCache: ManualPartsLookup | null = null
|
|
let manualPagesPartsCache: ManualPagesParts | null = null
|
|
|
|
/**
|
|
* Load parts lookup data
|
|
*/
|
|
async function loadPartsData(): Promise<{
|
|
manualParts: ManualPartsLookup
|
|
manualPagesParts: ManualPagesParts
|
|
}> {
|
|
if (manualPartsCache && manualPagesPartsCache) {
|
|
return {
|
|
manualParts: manualPartsCache,
|
|
manualPagesParts: manualPagesPartsCache,
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Try to load from public data directory (relative to public folder)
|
|
const [manualPartsResponse, manualPagesResponse] = await Promise.all([
|
|
fetch('/manual_parts_lookup.json'),
|
|
fetch('/manual_pages_parts.json'),
|
|
])
|
|
|
|
if (manualPartsResponse.ok && manualPagesResponse.ok) {
|
|
const [manualParts, manualPagesParts] = await Promise.all([
|
|
manualPartsResponse.json(),
|
|
manualPagesResponse.json(),
|
|
])
|
|
|
|
manualPartsCache = manualParts
|
|
manualPagesPartsCache = manualPagesParts
|
|
|
|
return { manualParts, manualPagesParts }
|
|
}
|
|
} catch (error) {
|
|
console.warn('Could not load parts data:', error)
|
|
}
|
|
|
|
// Return empty data if loading fails
|
|
return {
|
|
manualParts: {},
|
|
manualPagesParts: {},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search eBay for parts with caching
|
|
*/
|
|
async function searchEBayForParts(partNumber: string, description?: string, manufacturer?: string): Promise<any[]> {
|
|
const cacheKey = `parts:${partNumber}:${description || ''}:${manufacturer || ''}`
|
|
|
|
// Check cache
|
|
const cached = ebaySearchCache.get(cacheKey)
|
|
if (cached && Date.now() - cached.timestamp < EBAY_CACHE_TTL) {
|
|
return cached.results
|
|
}
|
|
|
|
try {
|
|
const results = await ebayClient.searchVendingParts(partNumber, description, manufacturer)
|
|
ebaySearchCache.set(cacheKey, { results, timestamp: Date.now() })
|
|
return results
|
|
} catch (error) {
|
|
console.error(`Error searching eBay for part ${partNumber}:`, error)
|
|
// Return empty array if API fails
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enhance parts data with real-time eBay listings
|
|
*/
|
|
async function enhancePartsData(parts: PartForPage[]): Promise<PartForPage[]> {
|
|
if (!ebayClient.isConfigured()) {
|
|
return parts
|
|
}
|
|
|
|
const enhancedParts = await Promise.all(parts.map(async (part) => {
|
|
// Only search for parts without existing eBay listings
|
|
if (part.ebayListings.length === 0) {
|
|
const ebayResults = await searchEBayForParts(part.partNumber, part.description)
|
|
part.ebayListings = ebayResults.map(result => ({
|
|
itemId: result.itemId,
|
|
title: result.title,
|
|
price: result.price,
|
|
currency: result.currency,
|
|
imageUrl: result.imageUrl,
|
|
viewItemUrl: result.viewItemUrl,
|
|
condition: result.condition,
|
|
shippingCost: result.shippingCost,
|
|
affiliateLink: result.affiliateLink,
|
|
}))
|
|
}
|
|
return part
|
|
}))
|
|
|
|
return enhancedParts
|
|
}
|
|
|
|
/**
|
|
* Get all parts for a manual with enhanced eBay data
|
|
*/
|
|
export async function getPartsForManual(
|
|
manualFilename: string
|
|
): Promise<PartForPage[]> {
|
|
const { manualParts } = await loadPartsData()
|
|
|
|
// Try exact match first
|
|
if (manualParts[manualFilename]) {
|
|
const parts = manualParts[manualFilename]
|
|
return enhancePartsData(parts)
|
|
}
|
|
|
|
// Try case-insensitive match
|
|
const lowerFilename = manualFilename.toLowerCase()
|
|
for (const [filename, parts] of Object.entries(manualParts)) {
|
|
if (filename.toLowerCase() === lowerFilename) {
|
|
return enhancePartsData(parts)
|
|
}
|
|
}
|
|
|
|
// Try partial match (filename without extension)
|
|
const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, '')
|
|
const lowerWithoutExt = filenameWithoutExt.toLowerCase()
|
|
|
|
for (const [filename, parts] of Object.entries(manualParts)) {
|
|
const otherWithoutExt = filename.replace(/\.pdf$/i, '').toLowerCase()
|
|
if (otherWithoutExt === lowerWithoutExt || otherWithoutExt.includes(lowerWithoutExt) || lowerWithoutExt.includes(otherWithoutExt)) {
|
|
return enhancePartsData(parts)
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Get parts for a specific page of a manual with enhanced eBay data
|
|
*/
|
|
export async function getPartsForPage(
|
|
manualFilename: string,
|
|
pageNumber: number
|
|
): Promise<PartForPage[]> {
|
|
const { manualPagesParts } = await loadPartsData()
|
|
|
|
// Try exact match first
|
|
if (manualPagesParts[manualFilename] && manualPagesParts[manualFilename][pageNumber.toString()]) {
|
|
const parts = manualPagesParts[manualFilename][pageNumber.toString()]
|
|
return enhancePartsData(parts)
|
|
}
|
|
|
|
// Try case-insensitive match
|
|
const lowerFilename = manualFilename.toLowerCase()
|
|
for (const [filename, pages] of Object.entries(manualPagesParts)) {
|
|
if (filename.toLowerCase() === lowerFilename) {
|
|
if (pages[pageNumber.toString()]) {
|
|
const parts = pages[pageNumber.toString()]
|
|
return enhancePartsData(parts)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try partial match
|
|
const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, '')
|
|
const lowerWithoutExt = filenameWithoutExt.toLowerCase()
|
|
|
|
for (const [filename, pages] of Object.entries(manualPagesParts)) {
|
|
const otherWithoutExt = filename.replace(/\.pdf$/i, '').toLowerCase()
|
|
if (otherWithoutExt === lowerWithoutExt || otherWithoutExt.includes(lowerWithoutExt) || lowerWithoutExt.includes(otherWithoutExt)) {
|
|
if (pages[pageNumber.toString()]) {
|
|
const parts = pages[pageNumber.toString()]
|
|
return enhancePartsData(parts)
|
|
}
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Get top N parts for a manual (most relevant) with enhanced eBay data
|
|
*/
|
|
export async function getTopPartsForManual(
|
|
manualFilename: string,
|
|
limit: number = 5
|
|
): Promise<PartForPage[]> {
|
|
const parts = await getPartsForManual(manualFilename)
|
|
|
|
// Sort by number of eBay listings (more listings = more relevant)
|
|
const sorted = parts.sort((a, b) => b.ebayListings.length - a.ebayListings.length)
|
|
|
|
return sorted.slice(0, limit)
|
|
}
|
|
|
|
/**
|
|
* Check if a manual has parts available
|
|
*/
|
|
export async function hasPartsForManual(manualFilename: string): Promise<boolean> {
|
|
const parts = await getPartsForManual(manualFilename)
|
|
return parts.length > 0 && parts.some(part => part.ebayListings.length > 0)
|
|
}
|
|
|
|
/**
|
|
* Get a set of all manual filenames that have parts available
|
|
* Useful for filtering
|
|
*/
|
|
export async function getManualsWithParts(): Promise<Set<string>> {
|
|
const { manualParts } = await loadPartsData()
|
|
const manualsWithParts = new Set<string>()
|
|
|
|
for (const [filename, parts] of Object.entries(manualParts)) {
|
|
if (parts.length > 0 && parts.some(part => part.ebayListings.length > 0)) {
|
|
manualsWithParts.add(filename)
|
|
// Also add variations for matching
|
|
manualsWithParts.add(filename.toLowerCase())
|
|
manualsWithParts.add(filename.replace(/\.pdf$/i, ''))
|
|
manualsWithParts.add(filename.replace(/\.pdf$/i, '').toLowerCase())
|
|
}
|
|
}
|
|
|
|
return manualsWithParts
|
|
}
|
|
|
|
/**
|
|
* Clear cache (useful for development)
|
|
*/
|
|
export function clearPartsCache(): void {
|
|
manualPartsCache = null
|
|
manualPagesPartsCache = null
|
|
}
|