/** * Line Items Search Page * Search consolidated line items with price change detection. */ import { useState, useMemo, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useAuth } from '../App' import IngredientModal from './IngredientModal' import { IngredientModalResult, LineItemResult } from '../utils/ingredientHelpers' import { useDebounce, usePersistedState, getDefaultDateRange, formatDateForDisplay, formatCurrency, formatQuantity, getPriceStatusConfig, SEARCH_STORAGE_KEYS, } from '../utils/searchHelpers' import LineItemHistoryModal from './LineItemHistoryModal' interface LineItemSearchItem { product_code: string | null description: string | null supplier_id: number | null supplier_name: string | null unit: string | null most_recent_price: number | null earliest_price_in_period: number | null price_change_percent: number | null price_change_status: string total_quantity: number | null occurrence_count: number most_recent_invoice_id: number most_recent_invoice_number: string | null most_recent_date: string | null has_definition: boolean portions_per_unit: number | null pack_quantity: number | null most_recent_line_item_id: number | null most_recent_line_number: number | null most_recent_raw_content: string | null most_recent_pack_quantity: number | null most_recent_unit_size: number | null most_recent_unit_size_type: string | null // Ingredient mapping ingredient_id: number | null ingredient_name: string | null ingredient_standard_unit: string | null price_per_std_unit: number | null } interface GroupSummary { name: string count: number total: number | null } interface SearchResponse { items: LineItemSearchItem[] total_count: number grouped_by: string | null groups: GroupSummary[] | null } interface Supplier { id: number name: string } interface SearchSettings { price_change_lookback_days: number price_change_amber_threshold: number price_change_red_threshold: number } interface IngredientSuggestion { id: number name: string category_name: string | null standard_unit: string yield_percent: number similarity?: number } const keys = SEARCH_STORAGE_KEYS.lineItems export default function SearchLineItems() { const { token } = useAuth() const queryClient = useQueryClient() const defaultDates = useMemo(() => getDefaultDateRange(), []) // Persisted search state const [searchInput, setSearchInput] = usePersistedState(keys.query, '') const [supplierId, setSupplierId] = usePersistedState(keys.supplier, '') const [dateFrom, setDateFrom] = usePersistedState(keys.dateFrom, defaultDates.from) const [dateTo, setDateTo] = usePersistedState(keys.dateTo, defaultDates.to) const [groupBy, setGroupBy] = usePersistedState(keys.groupBy, '') const [priceChangeFilter, setPriceChangeFilter] = usePersistedState(keys.priceChangeFilter, '') const [mappedFilter, setMappedFilter] = usePersistedState(keys.mappedFilter, '') const [sortColumn, setSortColumn] = usePersistedState(keys.sortColumn, '') const [sortDirection, setSortDirection] = usePersistedState<'asc' | 'desc'>(keys.sortDirection, 'asc') // History modal state const [historyModal, setHistoryModal] = useState<{ isOpen: boolean productCode: string | null description: string | null unit: string | null supplierId: number supplierName: string } | null>(null) // Ingredient mapping modal state (same as Review.tsx) const [modalItem, setModalItem] = useState(null) const [costEdits, setCostEdits] = useState<{ pack_quantity?: number | null; unit_size?: number | null; unit_size_type?: string | null; unit_price?: number | null }>({}) const [ingredientSearch, setIngredientSearch] = useState('') const [ingredientSuggestions, setIngredientSuggestions] = useState([]) const [ingredientSearchLoading, setIngredientSearchLoading] = useState(false) const [selectedIngredientId, setSelectedIngredientId] = useState(null) const [selectedIngredientName, setSelectedIngredientName] = useState('') const [selectedIngredientUnit, setSelectedIngredientUnit] = useState('') const [showCreateIngredient, setShowCreateIngredient] = useState(false) const [conversionDisplay, setConversionDisplay] = useState('') const [savingMapping, setSavingMapping] = useState(false) // Description alias suggestion state const [aliasSuggestions, setAliasSuggestions] = useState>({}) const [addingAliasFor, setAddingAliasFor] = useState(null) // Debounce search input const debouncedSearch = useDebounce(searchInput, 300) // Fetch suppliers const { data: suppliers } = useQuery({ queryKey: ['suppliers'], queryFn: async () => { const res = await fetch('/kitchen/api/suppliers/', { headers: { Authorization: `Bearer ${token}` }, }) if (!res.ok) throw new Error('Failed to fetch suppliers') return res.json() }, }) // Fetch search settings (for price change thresholds) const { data: _searchSettings } = useQuery({ queryKey: ['search-settings'], queryFn: async () => { const res = await fetch('/kitchen/api/search/settings', { headers: { Authorization: `Bearer ${token}` }, }) if (!res.ok) throw new Error('Failed to fetch search settings') return res.json() }, staleTime: 60000, // Cache for 1 minute }) // Search line items const { data, isLoading, error } = useQuery({ queryKey: ['search-line-items', debouncedSearch, supplierId, dateFrom, dateTo, groupBy, mappedFilter], queryFn: async () => { const params = new URLSearchParams() if (debouncedSearch) params.set('q', debouncedSearch) if (supplierId) params.set('supplier_id', supplierId) if (dateFrom) params.set('date_from', dateFrom) if (dateTo) params.set('date_to', dateTo) if (groupBy) params.set('group_by', groupBy) if (mappedFilter) params.set('mapped', mappedFilter) params.set('limit', '200') const res = await fetch(`/kitchen/api/search/line-items?${params}`, { headers: { Authorization: `Bearer ${token}` }, }) if (!res.ok) throw new Error('Search failed') return res.json() }, enabled: !!token, }) // Fetch alias suggestions for unmapped items without product codes useEffect(() => { if (!data?.items || !token) return // Group unmapped items by supplier for batch requests const bySupplier = new Map() for (const item of data.items) { if (!item.ingredient_id && !item.product_code && item.description && item.supplier_id) { if (!bySupplier.has(item.supplier_id)) bySupplier.set(item.supplier_id, []) bySupplier.get(item.supplier_id)!.push({ description: item.description, price: item.most_recent_price ?? undefined, }) } } if (bySupplier.size === 0) { setAliasSuggestions({}); return } const allSuggestions: typeof aliasSuggestions = {} const promises = Array.from(bySupplier.entries()).map(([sid, items]) => fetch('/kitchen/api/ingredients/sources/alias-suggestions', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ supplier_id: sid, items }), }) .then(res => res.ok ? res.json() : []) .then((suggestions: any[]) => { for (const s of suggestions) { allSuggestions[`${sid}:${s.description.toLowerCase()}`] = s } }) .catch(() => {}) ) Promise.all(promises).then(() => setAliasSuggestions({ ...allSuggestions })) }, [data?.items, token]) const addDescriptionAliasMutation = useMutation({ mutationFn: async ({ sourceId, alias }: { sourceId: number; alias: string }) => { const res = await fetch(`/kitchen/api/ingredients/sources/${sourceId}/aliases`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ alias }), }) if (!res.ok) throw new Error('Failed to add alias') return res.json() }, onSuccess: (_data, variables) => { setAliasSuggestions(prev => { const next = { ...prev } // Remove matching key for (const key of Object.keys(next)) { if (key.endsWith(`:${variables.alias.toLowerCase()}`)) delete next[key] } return next }) setAddingAliasFor(null) queryClient.invalidateQueries({ queryKey: ['search-line-items'] }) }, }) const openHistoryModal = (item: LineItemSearchItem) => { if (!item.supplier_id) return setHistoryModal({ isOpen: true, productCode: item.product_code, description: item.description, unit: item.unit, supplierId: item.supplier_id, supplierName: item.supplier_name || 'Unknown', }) } const handleScaleClick = async (item: LineItemSearchItem) => { setModalItem(item) setCostEdits({ pack_quantity: item.most_recent_pack_quantity ? Number(item.most_recent_pack_quantity) : null, unit_size: item.most_recent_unit_size ? Number(item.most_recent_unit_size) : null, unit_size_type: item.most_recent_unit_size_type, unit_price: item.most_recent_price != null ? Number(item.most_recent_price) : null, }) setSelectedIngredientId(item.ingredient_id || null) setSelectedIngredientName(item.ingredient_name || '') setSelectedIngredientUnit(item.ingredient_standard_unit || '') setShowCreateIngredient(false) setIngredientSearch('') setIngredientSuggestions([]) setConversionDisplay('') setSavingMapping(false) // Show conversion display if pack data exists if (item.most_recent_unit_size && item.most_recent_unit_size_type) { updateConversionDisplay(item.ingredient_standard_unit || undefined, { pack_quantity: item.most_recent_pack_quantity, unit_size: item.most_recent_unit_size, unit_size_type: item.most_recent_unit_size_type, unit_price: item.most_recent_price, }) } // Auto-suggest ingredient match from description if unmapped if (item.description && !item.ingredient_id) { searchIngredients(item.description) } } const searchIngredients = async (query: string) => { if (!query || query.length < 2) { setIngredientSuggestions([]) return } setIngredientSearchLoading(true) try { const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(query)}`, { headers: { Authorization: `Bearer ${token}` }, }) if (res.ok) setIngredientSuggestions(await res.json()) } catch { /* ignore */ } setIngredientSearchLoading(false) } const selectIngredient = (ing: IngredientSuggestion) => { setSelectedIngredientId(ing.id) setSelectedIngredientName(ing.name) setSelectedIngredientUnit(ing.standard_unit) setIngredientSearch('') setIngredientSuggestions([]) if (!costEdits.unit_size_type) { setCostEdits(prev => ({ ...prev, unit_size_type: ing.standard_unit })) } updateConversionDisplay(ing.standard_unit) } const handleIngredientCreated = (_result: IngredientModalResult) => { // IngredientModal already created the source mapping, so close everything queryClient.invalidateQueries({ queryKey: ['search-line-items'] }) closeModal() } const updateConversionDisplay = (stdUnit?: string, overrides?: Record) => { const edits = overrides ? { ...costEdits, ...overrides } : costEdits const unit = stdUnit || selectedIngredientUnit || (edits.unit_size_type as string) const pq = (edits.pack_quantity as number) || 1 const us = edits.unit_size as number const ust = edits.unit_size_type as string const up = edits.unit_price as number if (!us || !ust || !unit) { setConversionDisplay(''); return } const conversions: Record> = { g: { g: 1, kg: 0.001 }, kg: { g: 1000, kg: 1 }, oz: { g: 28.3495, kg: 0.0283495 }, ml: { ml: 1, ltr: 0.001 }, cl: { ml: 10, ltr: 0.01 }, ltr: { ml: 1000, ltr: 1 }, each: { each: 1 }, } const conv = conversions[ust]?.[unit] if (!conv) { setConversionDisplay(ust !== unit ? `Cannot convert ${ust} → ${unit}` : `${us}${ust}`); return } const totalStd = pq * us * conv const pricePerStd = up ? (up / totalStd) : null const packNote = pq > 1 ? `${pq} × ${us}${ust} = ` : '' let display = `${packNote}${totalStd.toFixed(totalStd % 1 ? 2 : 0)} ${unit}` if (pricePerStd) { display += ` → £${pricePerStd.toFixed(4)} per ${unit}` if (unit === 'g') display += ` (£${(pricePerStd * 1000).toFixed(2)}/kg)` else if (unit === 'ml') display += ` (£${(pricePerStd * 1000).toFixed(2)}/ltr)` } setConversionDisplay(display) } const closeModal = () => { setModalItem(null) setCostEdits({}) setSelectedIngredientId(null) setSelectedIngredientName('') setSelectedIngredientUnit('') setShowCreateIngredient(false) setIngredientSearch('') setIngredientSuggestions([]) setConversionDisplay('') } const saveMapping = async () => { if (!modalItem) return setSavingMapping(true) try { // Update the most recent line item's pack fields if we have one if (modalItem.most_recent_line_item_id && modalItem.most_recent_invoice_id) { await fetch(`/kitchen/api/invoices/${modalItem.most_recent_invoice_id}/line-items/${modalItem.most_recent_line_item_id}`, { method: 'PATCH', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ pack_quantity: costEdits.pack_quantity || null, unit_size: costEdits.unit_size || null, unit_size_type: costEdits.unit_size_type || null, ...(selectedIngredientId ? { ingredient_id: selectedIngredientId } : {}), }), }) } // Create ingredient source mapping if (selectedIngredientId && modalItem.supplier_id) { const sourceData: Record = { supplier_id: modalItem.supplier_id, pack_quantity: costEdits.pack_quantity || 1, unit_size: costEdits.unit_size || null, unit_size_type: costEdits.unit_size_type || selectedIngredientUnit || null, } if (modalItem.product_code) { sourceData.product_code = modalItem.product_code } else if (modalItem.description) { sourceData.description_pattern = modalItem.description.substring(0, 100).toLowerCase().trim() } if (costEdits.unit_price) sourceData.latest_unit_price = costEdits.unit_price if (modalItem.most_recent_invoice_id) sourceData.invoice_id = modalItem.most_recent_invoice_id await fetch(`/kitchen/api/ingredients/${selectedIngredientId}/sources`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(sourceData), }) } queryClient.invalidateQueries({ queryKey: ['search-line-items'] }) closeModal() } catch (err) { console.error('Failed to save mapping:', err) } setSavingMapping(false) } const handleSort = (column: string) => { if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') } else { setSortColumn(column) setSortDirection('asc') } } const getSortIndicator = (column: string) => { if (sortColumn !== column) return null return sortDirection === 'asc' ? ' ▲' : ' ▼' } // Filter items by price change status const filteredItems = useMemo(() => { if (!data?.items) return [] if (!priceChangeFilter) return data.items return data.items.filter((item) => { if (priceChangeFilter === 'no_history') { return item.price_change_status === 'no_history' } else if (priceChangeFilter === 'no_change') { return item.price_change_status === 'consistent' || item.price_change_status === 'acknowledged' } else if (priceChangeFilter === 'increase') { return item.price_change_percent !== null && item.price_change_percent > 0 } else if (priceChangeFilter === 'decrease') { return item.price_change_percent !== null && item.price_change_percent < 0 } return true }) }, [data?.items, priceChangeFilter]) // Sort filtered items const sortedItems = useMemo(() => { if (!filteredItems.length || !sortColumn) return filteredItems const sorted = [...filteredItems].sort((a, b) => { let aVal: any let bVal: any switch (sortColumn) { case 'code': aVal = a.product_code || '' bVal = b.product_code || '' break case 'description': aVal = a.description || '' bVal = b.description || '' break case 'supplier': aVal = a.supplier_name || '' bVal = b.supplier_name || '' break case 'price': aVal = a.most_recent_price ?? -Infinity bVal = b.most_recent_price ?? -Infinity break case 'ingredient_rate': aVal = a.price_per_std_unit != null ? (typeof a.price_per_std_unit === 'string' ? parseFloat(a.price_per_std_unit) : a.price_per_std_unit) : -Infinity bVal = b.price_per_std_unit != null ? (typeof b.price_per_std_unit === 'string' ? parseFloat(b.price_per_std_unit) : b.price_per_std_unit) : -Infinity break case 'total_quantity': aVal = a.total_quantity ?? -Infinity bVal = b.total_quantity ?? -Infinity break case 'occurrence_count': aVal = a.occurrence_count ?? 0 bVal = b.occurrence_count ?? 0 break case 'last_invoice': aVal = a.most_recent_invoice_number || '' bVal = b.most_recent_invoice_number || '' break case 'last_date': aVal = a.most_recent_date || '' bVal = b.most_recent_date || '' break default: return 0 } if (typeof aVal === 'string' && typeof bVal === 'string') { return sortDirection === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal) } else { return sortDirection === 'asc' ? aVal - bVal : bVal - aVal } }) return sorted }, [filteredItems, sortColumn, sortDirection]) const renderPriceStatus = (item: LineItemSearchItem) => { const config = getPriceStatusConfig(item.price_change_status) if (!config.icon) return null // Show icon for consistent prices (green tick) if (item.price_change_status === 'consistent' || item.price_change_status === 'acknowledged') { // Calculate lookback days from search period const searchPeriodDays = Math.ceil((new Date(dateTo).getTime() - new Date(dateFrom).getTime()) / (1000 * 60 * 60 * 24)) const lookbackDays = searchPeriodDays + 30 return (
{config.icon}
) } // Show icon + percentage for amber/red status with actual price changes if ((item.price_change_status === 'amber' || item.price_change_status === 'red') && item.price_change_percent !== null && item.price_change_percent !== 0) { const isIncrease = item.price_change_percent > 0 const arrow = isIncrease ? '▲' : '▼' const color = isIncrease ? '#ef4444' : '#22c55e' // Red for increase, green for decrease const direction = isIncrease ? 'increased' : 'decreased' // Calculate lookback days from search period const searchPeriodDays = Math.ceil((new Date(dateTo).getTime() - new Date(dateFrom).getTime()) / (1000 * 60 * 60 * 24)) const lookbackDays = searchPeriodDays + 30 return (
{arrow}{' '} {Math.abs(item.price_change_percent).toFixed(1)}%
) } // Show just the icon for amber/red without percentage if (item.price_change_status === 'amber' || item.price_change_status === 'red') { return (
{config.icon}
) } return null } const formatRate = (item: LineItemSearchItem) => { if (item.price_per_std_unit == null) return null const rate = typeof item.price_per_std_unit === 'string' ? parseFloat(item.price_per_std_unit) : item.price_per_std_unit const unit = item.ingredient_standard_unit || '?' // Convert tiny per-g/ml prices to per-kg/ltr for readability if (unit === 'g' && rate < 1) return `£${(rate * 1000).toFixed(2)}/kg` if (unit === 'ml' && rate < 1) return `£${(rate * 1000).toFixed(2)}/ltr` // For prices >= £1 show 2 decimals, for smaller show 4 const decimals = rate >= 1 ? 2 : 4 return `£${rate.toFixed(decimals)}/${unit}` } const clearSearch = () => { setSearchInput('') setSupplierId('') setPriceChangeFilter('') setMappedFilter('') setGroupBy('') const defaultDates = getDefaultDateRange() setDateFrom(defaultDates.from) setDateTo(defaultDates.to) } const hasActiveFilters = searchInput || supplierId || priceChangeFilter || mappedFilter || groupBy return (

Search Line Items

{/* Search Controls */}
setSearchInput(e.target.value)} style={{ flex: '1 1 300px', padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '4px', fontSize: '14px', }} />
setDateFrom(e.target.value)} style={{ padding: '6px', border: '1px solid #d1d5db', borderRadius: '4px' }} /> setDateTo(e.target.value)} style={{ padding: '6px', border: '1px solid #d1d5db', borderRadius: '4px' }} />
{hasActiveFilters && ( )}
{/* Legend */}
Price consistent Price increase Price decrease Mapped to ingredient Not mapped
{/* Results */} {isLoading &&

Loading...

} {error &&

Error: {(error as Error).message}

} {data && (

Found {data.total_count} unique item{data.total_count !== 1 ? 's' : ''} {(priceChangeFilter || mappedFilter) && ` (showing ${sortedItems.length} filtered)`}

{sortedItems.map((item, idx) => ( ))}
handleSort('code')}> Code{getSortIndicator('code')} handleSort('description')}> Description{getSortIndicator('description')} handleSort('supplier')}> Supplier{getSortIndicator('supplier')} handleSort('price')}> Price{getSortIndicator('price')} handleSort('ingredient_rate')}> Ingredient Rate{getSortIndicator('ingredient_rate')} handleSort('total_quantity')}> Total Qty{getSortIndicator('total_quantity')} handleSort('occurrence_count')}> #{getSortIndicator('occurrence_count')} handleSort('last_invoice')}> Last Invoice{getSortIndicator('last_invoice')} handleSort('last_date')}> Last Date{getSortIndicator('last_date')}
{item.product_code || '-'}
{item.description || '-'} {item.has_definition && ( 📦 )}
{!item.ingredient_id && !item.product_code && item.description && item.supplier_id && (() => { const key = `${item.supplier_id}:${item.description.toLowerCase()}` const suggestion = aliasSuggestions[key] if (!suggestion) return null const isAdding = addingAliasFor === key return (
Similar to "{suggestion.canonical_description}" {suggestion.ingredient_name && <> → {suggestion.ingredient_name}} {' '}({Math.round(suggestion.similarity * 100)}%)
) })()}
{item.supplier_name || '-'}
{formatCurrency(item.most_recent_price)} openHistoryModal(item)} style={{ marginLeft: '6px', cursor: 'pointer', }} title="View price history" > 📊
{renderPriceStatus(item)}
{item.ingredient_id ? (
{formatRate(item) || '-'}
{item.ingredient_name}
) : ( - )}
{formatQuantity(item.total_quantity)} {item.occurrence_count} {item.most_recent_invoice_number || '-'} {formatDateForDisplay(item.most_recent_date)}
)} {/* History Modal */} {historyModal && ( setHistoryModal(null)} productCode={historyModal.productCode} description={historyModal.description} unit={historyModal.unit} supplierId={historyModal.supplierId} supplierName={historyModal.supplierName} /> )} {/* Ingredient Mapping Modal (same as Review.tsx) */} {modalItem && (
e.stopPropagation()}>

Ingredient Mapping

{/* Line item context */}
{modalItem.description || modalItem.product_code || 'Unknown item'} {modalItem.supplier_name && ({modalItem.supplier_name})} {modalItem.most_recent_price != null && £{Number(modalItem.most_recent_price).toFixed(2)}}
{/* Ingredient mapping section */}
{selectedIngredientId ? (
{selectedIngredientName} ({selectedIngredientUnit})
) : ( <>
{ setIngredientSearch(e.target.value); searchIngredients(e.target.value) }} placeholder="Search ingredients..." style={{ padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '4px', fontSize: '14px', width: '100%', boxSizing: 'border-box' }} /> {ingredientSearchLoading && ...}
{ingredientSuggestions.length > 0 && (
{ingredientSuggestions.map((s) => (
selectIngredient(s)} style={{ padding: '0.5rem 0.75rem', cursor: 'pointer', borderBottom: '1px solid #f0f0f0', fontSize: '0.9rem', display: 'flex', justifyContent: 'space-between' }} onMouseOver={(e) => (e.currentTarget.style.background = '#f0f7ff')} onMouseOut={(e) => (e.currentTarget.style.background = '#fff')} > {s.name} ({s.standard_unit}) {s.category_name || ''} {s.similarity ? `${(s.similarity * 100).toFixed(0)}%` : ''}
))}
)} )}
{/* Unit conversion fields */}
{modalItem.description}{modalItem.most_recent_price != null ? ` @ £${Number(modalItem.most_recent_price).toFixed(2)}` : ''}
{ const v = { ...costEdits, unit_size: parseFloat(e.target.value) || null }; setCostEdits(v); updateConversionDisplay(undefined, v) }} onFocus={(e) => e.target.select()} style={{ padding: '6px 8px', border: '1px solid #d1d5db', borderRadius: '4px', width: '70px', fontSize: '14px' }} placeholder="e.g., 2" /> {selectedIngredientUnit && costEdits.unit_size_type && costEdits.unit_size_type !== selectedIngredientUnit && ( → {selectedIngredientUnit} )}
{ const v = { ...costEdits, pack_quantity: parseInt(e.target.value) || null }; setCostEdits(v); updateConversionDisplay(undefined, v) }} onFocus={(e) => e.target.select()} style={{ padding: '6px 8px', border: '1px solid #d1d5db', borderRadius: '4px', width: '50px', fontSize: '14px' }} placeholder="1" />
{costEdits.unit_price ? `£${Number(costEdits.unit_price).toFixed(2)}` : '--'}
{conversionDisplay && (
{conversionDisplay}
)}
{/* Actions */}
)} setShowCreateIngredient(false)} onSaved={handleIngredientCreated} prePopulateName={ingredientSearch || ''} preSelectLineItem={modalItem ? { product_code: modalItem.product_code, description: modalItem.description, supplier_id: modalItem.supplier_id, supplier_name: modalItem.supplier_name, unit: modalItem.unit, most_recent_price: modalItem.most_recent_price != null ? Number(modalItem.most_recent_price) : null, total_quantity: modalItem.total_quantity, occurrence_count: modalItem.occurrence_count, most_recent_invoice_id: modalItem.most_recent_invoice_id, most_recent_line_number: modalItem.most_recent_line_number, most_recent_pack_quantity: modalItem.most_recent_pack_quantity, most_recent_unit_size: modalItem.most_recent_unit_size, most_recent_unit_size_type: modalItem.most_recent_unit_size_type, ingredient_id: modalItem.ingredient_id, ingredient_name: modalItem.ingredient_name, } as LineItemResult : null} />
) } const thStyle: React.CSSProperties = { padding: '12px', textAlign: 'left', fontWeight: 600, fontSize: '14px', } const thStyleClickable: React.CSSProperties = { ...thStyle, cursor: 'pointer', userSelect: 'none', } const tdStyle: React.CSSProperties = { padding: '12px', fontSize: '14px', }