feat: F021 market intelligence & signal discovery
Signal domain types, mock data (8 signals), provider interface + MockupMarketIntelligenceProvider, marketIntelligenceService, TanStack Query hooks, 10 ops components (SignalInbox, MarketSignalCard, MarketSignalFilterBar, MarketSignalSkeleton, MarketSignalEmptyState, SignalConfidenceBadge, SourceReliabilityBadge, SensitivityWarningPanel, SignalConversionPanel, SignalEvidenceList), MarketIntelligence page wired into AppShell nav and App router. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import type { IMarketIntelligenceProvider } from './IMarketIntelligenceProvider'
|
||||
import type { MarketSignal, MarketSignalFilters, SignalProcessingStatus } from '../domain/marketSignal'
|
||||
import { MOCK_MARKET_SIGNALS } from '../mock-data/marketSignals'
|
||||
|
||||
// Mutable in-memory copy for status updates
|
||||
let signals: MarketSignal[] = [...MOCK_MARKET_SIGNALS]
|
||||
|
||||
function applyFilters(data: MarketSignal[], filters?: MarketSignalFilters): MarketSignal[] {
|
||||
if (!filters) return data
|
||||
return data.filter((s) => {
|
||||
if (filters.sourceCategory && s.sourceCategory !== filters.sourceCategory) return false
|
||||
if (filters.processingStatus && s.processingStatus !== filters.processingStatus) return false
|
||||
if (filters.sensitivityLevel && s.sensitivityLevel !== filters.sensitivityLevel) return false
|
||||
if (filters.signalType && s.signalType !== filters.signalType) return false
|
||||
if (filters.search) {
|
||||
const q = filters.search.toLowerCase()
|
||||
const match = s.title.toLowerCase().includes(q)
|
||||
|| s.summary.toLowerCase().includes(q)
|
||||
|| s.location.toLowerCase().includes(q)
|
||||
if (!match) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export const MockupMarketIntelligenceProvider: IMarketIntelligenceProvider = {
|
||||
async getSignals(filters?: MarketSignalFilters): Promise<MarketSignal[]> {
|
||||
return applyFilters(signals, filters)
|
||||
},
|
||||
|
||||
async getSignalById(id: string): Promise<MarketSignal | null> {
|
||||
return signals.find((s) => s.id === id) ?? null
|
||||
},
|
||||
|
||||
async updateSignalStatus(id: string, status: SignalProcessingStatus): Promise<MarketSignal> {
|
||||
const idx = signals.findIndex((s) => s.id === id)
|
||||
if (idx === -1) throw new Error(`Signal ${id} not found`)
|
||||
signals[idx] = { ...signals[idx], processingStatus: status, updatedAt: new Date().toISOString() }
|
||||
return signals[idx]
|
||||
},
|
||||
|
||||
async convertToFutureSignal(id: string): Promise<{ futureSignalId: string }> {
|
||||
const futureSignalId = `fs-${id}-${Date.now()}`
|
||||
const idx = signals.findIndex((s) => s.id === id)
|
||||
if (idx !== -1) {
|
||||
signals[idx] = {
|
||||
...signals[idx],
|
||||
processingStatus: 'CONVERTED_TO_FUTURE_AVAILABILITY',
|
||||
possibleFutureSignalId: futureSignalId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
return { futureSignalId }
|
||||
},
|
||||
|
||||
async linkSignalToEntity(
|
||||
id: string,
|
||||
entityType: 'property' | 'need',
|
||||
entityId: string,
|
||||
): Promise<MarketSignal> {
|
||||
const idx = signals.findIndex((s) => s.id === id)
|
||||
if (idx === -1) throw new Error(`Signal ${id} not found`)
|
||||
signals[idx] = {
|
||||
...signals[idx],
|
||||
...(entityType === 'property' ? { linkedPropertyId: entityId } : { linkedNeedId: entityId }),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
return signals[idx]
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user