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:
@@ -0,0 +1,18 @@
|
||||
export interface DashboardStats {
|
||||
totalProperties: number
|
||||
verifiedProperties: number
|
||||
externalMarketProperties: number
|
||||
futureSignalProperties: number
|
||||
totalMatches: number
|
||||
pendingReviews: number
|
||||
approvedMatches: number
|
||||
activeNeeds: number
|
||||
totalSignals: number
|
||||
verifiedSignals: number
|
||||
averageMatchScore: number
|
||||
highConfidenceMatches: number
|
||||
}
|
||||
|
||||
export interface IDashboardProvider {
|
||||
getStats(organizationId?: string): Promise<DashboardStats>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ReviewQueueItem, ReviewPriority, ReviewQueueStatus } from '../domain/review'
|
||||
|
||||
export interface ReviewFilters {
|
||||
priority?: ReviewPriority
|
||||
status?: ReviewQueueStatus
|
||||
assignedTo?: string
|
||||
organizationId?: string
|
||||
}
|
||||
|
||||
export interface IReviewProvider {
|
||||
getQueue(filters?: ReviewFilters): Promise<ReviewQueueItem[]>
|
||||
getById(id: string): Promise<ReviewQueueItem | null>
|
||||
approve(id: string, reviewedBy: string, notes?: string): Promise<ReviewQueueItem>
|
||||
reject(id: string, reviewedBy: string, notes?: string): Promise<ReviewQueueItem>
|
||||
assign(id: string, assignTo: string): Promise<ReviewQueueItem>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput } from '../domain/shortlist'
|
||||
import type { ShortlistStatus } from '../domain/enums'
|
||||
|
||||
export interface ShortlistFilters {
|
||||
needId?: string
|
||||
status?: ShortlistStatus
|
||||
createdBy?: string
|
||||
organizationId?: string
|
||||
}
|
||||
|
||||
export interface IShortlistProvider {
|
||||
getAll(filters?: ShortlistFilters): Promise<Shortlist[]>
|
||||
getById(id: string): Promise<Shortlist | null>
|
||||
create(data: CreateShortlistInput): Promise<Shortlist>
|
||||
update(id: string, data: UpdateShortlistInput): Promise<Shortlist>
|
||||
addItem(id: string, propertyId: string, note?: string): Promise<Shortlist>
|
||||
removeItem(id: string, propertyId: string): Promise<Shortlist>
|
||||
remove(id: string): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { IDashboardProvider, DashboardStats } from './IDashboardProvider'
|
||||
import { mockProperties } from '../mock-data/properties'
|
||||
import { mockMatches } from '../mock-data/matches'
|
||||
import { mockNeeds } from '../mock-data/needs'
|
||||
import { mockFutureSignals } from '../mock-data/futureSignals'
|
||||
import { mockReviewQueue } from '../mock-data/reviewQueue'
|
||||
import { mockDelay } from '../lib/mockUtils'
|
||||
|
||||
export const MockupDashboardProvider: IDashboardProvider = {
|
||||
async getStats(organizationId?) {
|
||||
await mockDelay()
|
||||
|
||||
let props = mockProperties
|
||||
let matches = mockMatches
|
||||
let needs = mockNeeds
|
||||
let signals = mockFutureSignals
|
||||
let queue = mockReviewQueue
|
||||
|
||||
if (organizationId) {
|
||||
props = props.filter(p => p.organizationId === organizationId)
|
||||
matches = matches.filter(m => m.organizationId === organizationId)
|
||||
needs = needs.filter(n => n.organizationId === organizationId)
|
||||
signals = signals.filter(s => s.organizationId === organizationId)
|
||||
queue = queue.filter(r => r.organizationId === organizationId)
|
||||
}
|
||||
|
||||
const avgScore =
|
||||
matches.length > 0
|
||||
? matches.reduce((sum, m) => sum + m.matchScore, 0) / matches.length
|
||||
: 0
|
||||
|
||||
const stats: DashboardStats = {
|
||||
totalProperties: props.length,
|
||||
verifiedProperties: props.filter(p => p.resultType === 'VERIFIED_PORTFOLIO').length,
|
||||
externalMarketProperties: props.filter(p => p.resultType === 'EXTERNAL_MARKET').length,
|
||||
futureSignalProperties: props.filter(p => p.resultType === 'FUTURE_AVAILABILITY').length,
|
||||
totalMatches: matches.length,
|
||||
pendingReviews: queue.filter(r => r.status === 'PENDING').length,
|
||||
approvedMatches: matches.filter(m => m.isApproved === true).length,
|
||||
activeNeeds: needs.length,
|
||||
totalSignals: signals.length,
|
||||
verifiedSignals: signals.filter(s => s.isVerified).length,
|
||||
averageMatchScore: Math.round(avgScore),
|
||||
highConfidenceMatches: matches.filter(m => m.confidenceLevel >= 0.75).length,
|
||||
}
|
||||
return stats
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { IReviewProvider, ReviewFilters } from './IReviewProvider'
|
||||
import type { ReviewQueueItem } from '../domain/review'
|
||||
import { mockReviewQueue } from '../mock-data/reviewQueue'
|
||||
import { mockDelay } from '../lib/mockUtils'
|
||||
|
||||
const store: ReviewQueueItem[] = [...mockReviewQueue]
|
||||
|
||||
const priorityOrder: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 }
|
||||
|
||||
export const MockupReviewProvider: IReviewProvider = {
|
||||
async getQueue(filters?: ReviewFilters) {
|
||||
await mockDelay()
|
||||
let results = [...store]
|
||||
if (filters?.priority) results = results.filter(r => r.priority === filters.priority)
|
||||
if (filters?.status) results = results.filter(r => r.status === filters.status)
|
||||
if (filters?.assignedTo) results = results.filter(r => r.assignedTo === filters.assignedTo)
|
||||
if (filters?.organizationId) results = results.filter(r => r.organizationId === filters.organizationId)
|
||||
return results.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9))
|
||||
},
|
||||
async getById(id) {
|
||||
await mockDelay()
|
||||
return store.find(r => r.id === id) ?? null
|
||||
},
|
||||
async approve(id, _reviewedBy, notes?) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(r => r.id === id)
|
||||
store[idx] = { ...store[idx], status: 'COMPLETED', notes, updatedAt: new Date().toISOString() }
|
||||
return store[idx]
|
||||
},
|
||||
async reject(id, _reviewedBy, notes?) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(r => r.id === id)
|
||||
store[idx] = { ...store[idx], status: 'COMPLETED', notes, updatedAt: new Date().toISOString() }
|
||||
return store[idx]
|
||||
},
|
||||
async assign(id, assignTo) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(r => r.id === id)
|
||||
store[idx] = { ...store[idx], assignedTo: assignTo, status: 'IN_REVIEW', updatedAt: new Date().toISOString() }
|
||||
return store[idx]
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IShortlistProvider, ShortlistFilters } from './IShortlistProvider'
|
||||
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput } from '../domain/shortlist'
|
||||
import { mockShortlists } from '../mock-data/shortlists'
|
||||
import { mockDelay } from '../lib/mockUtils'
|
||||
|
||||
const store: Shortlist[] = [...mockShortlists]
|
||||
|
||||
export const MockupShortlistProvider: IShortlistProvider = {
|
||||
async getAll(filters?: ShortlistFilters) {
|
||||
await mockDelay()
|
||||
let results = [...store]
|
||||
if (filters?.needId) results = results.filter(s => s.needId === filters.needId)
|
||||
if (filters?.status) results = results.filter(s => s.status === filters.status)
|
||||
if (filters?.createdBy) results = results.filter(s => s.createdBy === filters.createdBy)
|
||||
if (filters?.organizationId) results = results.filter(s => s.organizationId === filters.organizationId)
|
||||
return results
|
||||
},
|
||||
async getById(id) {
|
||||
await mockDelay()
|
||||
return store.find(s => s.id === id) ?? null
|
||||
},
|
||||
async create(data: CreateShortlistInput) {
|
||||
await mockDelay()
|
||||
const next: Shortlist = {
|
||||
id: crypto.randomUUID(),
|
||||
...data,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
store.push(next)
|
||||
return next
|
||||
},
|
||||
async update(id, data: UpdateShortlistInput) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(s => s.id === id)
|
||||
store[idx] = { ...store[idx], ...data, updatedAt: new Date().toISOString() }
|
||||
return store[idx]
|
||||
},
|
||||
async addItem(id, propertyId, note?) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(s => s.id === id)
|
||||
const alreadyAdded = store[idx].items.some(i => i.propertyId === propertyId)
|
||||
if (!alreadyAdded) {
|
||||
store[idx] = {
|
||||
...store[idx],
|
||||
items: [...store[idx].items, { propertyId, addedAt: new Date().toISOString(), note }],
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
return store[idx]
|
||||
},
|
||||
async removeItem(id, propertyId) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(s => s.id === id)
|
||||
store[idx] = {
|
||||
...store[idx],
|
||||
items: store[idx].items.filter(i => i.propertyId !== propertyId),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
return store[idx]
|
||||
},
|
||||
async remove(id) {
|
||||
await mockDelay()
|
||||
const idx = store.findIndex(s => s.id === id)
|
||||
store.splice(idx, 1)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user