feat(services): F005 – complete service layer
New domain types: - shortlist.ts: Shortlist, ShortlistItem, CreateShortlistInput - review.ts: ReviewQueueItem, ReviewPriority, ReviewQueueStatus New mock data: - shortlists.ts: 2 sample shortlists - reviewQueue.ts: 3 pending review items New providers (interface + mockup): - IShortlistProvider / MockupShortlistProvider (with 150ms latency) - IReviewProvider / MockupReviewProvider (priority-sorted, with latency) - IDashboardProvider / MockupDashboardProvider (computed from all mock data) New services: - reviewService: getQueue, approve, reject, assign - shortlistService: CRUD + addItem/removeItem - authService: wraps sessionStore, swappable for real auth - dashboardService: getStats(organizationId?) Enhancements: - types.ts: typed ServiceErrorCode + ServiceError, Pagination alias - aiService.ts: openRouterAIService stub (throws ai_generation_failed) - lib/mockUtils.ts: mockDelay(ms) utility New hooks: - useUnifiedResults(needId?): joins matches+properties+signals → UnifiedMatchResult[] - useReviewQueue: + useApproveReviewItem + useRejectReviewItem - useMatchDetail, usePropertyDetail, useNeedProfiles aliases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -1,4 +1,6 @@
|
||||
export { useProperties, useProperty } from './useProperties'
|
||||
export { useMatches, useMatchesByNeed, useMatchesByProperty, useApproveMatch } from './useMatches'
|
||||
export { useProperties, useProperty, usePropertyDetail } from './useProperties'
|
||||
export { useMatches, useMatchesByNeed, useMatchesByProperty, useApproveMatch, useMatchDetail } from './useMatches'
|
||||
export { useFutureSignals, useFutureSignalsByProperty, useVerifySignal } from './useFutureSignals'
|
||||
export { useNeeds, useNeed } from './useNeeds'
|
||||
export { useNeeds, useNeed, useNeedProfiles } from './useNeeds'
|
||||
export { useUnifiedResults } from './useUnifiedResults'
|
||||
export { useReviewQueue, useApproveReviewItem, useRejectReviewItem } from './useReviewQueue'
|
||||
|
||||
@@ -40,3 +40,13 @@ export function useApproveMatch() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useMatchDetail(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['match', id],
|
||||
queryFn: () => matchService.getById(id),
|
||||
staleTime: STALE_MATCHES,
|
||||
enabled: Boolean(id),
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,3 +17,5 @@ export function useNeed(id: string) {
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
export const useNeedProfiles = useNeeds
|
||||
|
||||
@@ -30,3 +30,5 @@ export function useProperty(id: string) {
|
||||
select: (res) => res.data ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePropertyDetail = useProperty
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { reviewService } from '../services/reviewService'
|
||||
import type { ReviewFilters } from '../provider/IReviewProvider'
|
||||
|
||||
const STALE_REVIEW = 30_000
|
||||
|
||||
export function useReviewQueue(filters?: ReviewFilters) {
|
||||
return useQuery({
|
||||
queryKey: ['reviewQueue', filters ?? {}],
|
||||
queryFn: () => reviewService.getQueue(filters),
|
||||
staleTime: STALE_REVIEW,
|
||||
select: (res) => res.data ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
export function useApproveReviewItem() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, notes }: { id: string; notes?: string }) =>
|
||||
reviewService.approve(id, 'current-user', notes),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['reviewQueue'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRejectReviewItem() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, notes }: { id: string; notes?: string }) =>
|
||||
reviewService.reject(id, 'current-user', notes),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['reviewQueue'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useMatches, useMatchesByNeed } from './useMatches'
|
||||
import { useProperties } from './useProperties'
|
||||
import { useFutureSignals } from './useFutureSignals'
|
||||
import type {
|
||||
UnifiedMatchResult,
|
||||
VerifiedPortfolioResult,
|
||||
ExternalMarketResult,
|
||||
FutureAvailabilityResult,
|
||||
} from '../domain/unifiedResult'
|
||||
|
||||
export function useUnifiedResults(needId?: string) {
|
||||
const allMatchesQuery = useMatches()
|
||||
const needMatchesQuery = useMatchesByNeed(needId ?? '')
|
||||
const propertiesQuery = useProperties()
|
||||
const signalsQuery = useFutureSignals()
|
||||
|
||||
const matchesQuery = needId ? needMatchesQuery : allMatchesQuery
|
||||
|
||||
const isLoading =
|
||||
matchesQuery.isLoading || propertiesQuery.isLoading || signalsQuery.isLoading
|
||||
const error = matchesQuery.error ?? propertiesQuery.error ?? signalsQuery.error
|
||||
|
||||
const matches = matchesQuery.data ?? []
|
||||
const properties = propertiesQuery.data ?? []
|
||||
const signals = signalsQuery.data ?? []
|
||||
|
||||
const data = useMemo((): UnifiedMatchResult[] => {
|
||||
return matches
|
||||
.flatMap((match): UnifiedMatchResult[] => {
|
||||
const rt = match.resultType ?? 'VERIFIED_PORTFOLIO'
|
||||
|
||||
if (rt === 'FUTURE_AVAILABILITY') {
|
||||
const refId = match.resultId ?? match.propertyId
|
||||
const signal = signals.find(
|
||||
s => s.id === refId || s.propertyId === refId,
|
||||
)
|
||||
if (!signal) return []
|
||||
const result: FutureAvailabilityResult = {
|
||||
matchId: match.id,
|
||||
needId: match.needId,
|
||||
matchScore: match.matchScore,
|
||||
resultType: 'FUTURE_AVAILABILITY',
|
||||
signal,
|
||||
match,
|
||||
}
|
||||
return [result]
|
||||
}
|
||||
|
||||
const property = properties.find(p => p.id === (match.resultId ?? match.propertyId))
|
||||
if (!property) return []
|
||||
|
||||
if (rt === 'EXTERNAL_MARKET') {
|
||||
const result: ExternalMarketResult = {
|
||||
matchId: match.id,
|
||||
needId: match.needId,
|
||||
matchScore: match.matchScore,
|
||||
resultType: 'EXTERNAL_MARKET',
|
||||
property,
|
||||
match,
|
||||
}
|
||||
return [result]
|
||||
}
|
||||
|
||||
const result: VerifiedPortfolioResult = {
|
||||
matchId: match.id,
|
||||
needId: match.needId,
|
||||
matchScore: match.matchScore,
|
||||
resultType: 'VERIFIED_PORTFOLIO',
|
||||
property,
|
||||
match,
|
||||
}
|
||||
return [result]
|
||||
})
|
||||
.sort((a, b) => b.matchScore - a.matchScore)
|
||||
}, [matches, properties, signals])
|
||||
|
||||
return { data, isLoading, error }
|
||||
}
|
||||
Reference in New Issue
Block a user