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:
@@ -1,4 +1,5 @@
|
||||
import type { ItemResponse } from './types'
|
||||
import type { ItemResponse, ServiceError } from './types'
|
||||
import { ServiceErrorCode } from './types'
|
||||
import type { CreateNeedInput } from '../domain/need'
|
||||
|
||||
export interface CriteriaExtractionResult {
|
||||
@@ -45,6 +46,21 @@ const MockupAIServiceProvider: AIServiceProvider = {
|
||||
|
||||
const provider = MockupAIServiceProvider
|
||||
|
||||
const notConfiguredError = (): ServiceError => ({
|
||||
code: ServiceErrorCode.AI_GENERATION_FAILED,
|
||||
message: 'OpenRouter nicht konfiguriert',
|
||||
})
|
||||
|
||||
// Stub — swap for real OpenRouter implementation without changing call sites
|
||||
export const openRouterAIService: AIServiceProvider = {
|
||||
async extractCriteria(_input: string): Promise<CriteriaExtractionResult> {
|
||||
throw notConfiguredError()
|
||||
},
|
||||
async generateFollowUp(_partialNeed: Partial<CreateNeedInput>): Promise<string[]> {
|
||||
throw notConfiguredError()
|
||||
},
|
||||
}
|
||||
|
||||
export const aiService = {
|
||||
async extractCriteria(input: string): Promise<ItemResponse<CriteriaExtractionResult>> {
|
||||
const data = await provider.extractCriteria(input)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useSessionStore } from '../stores/sessionStore'
|
||||
import type { MockUser } from '../stores/sessionStore'
|
||||
import type { ItemResponse } from './types'
|
||||
import { UserRole } from '../domain/enums'
|
||||
|
||||
export const authService = {
|
||||
async getCurrentUser(): Promise<ItemResponse<MockUser | null>> {
|
||||
const data = useSessionStore.getState().currentUser
|
||||
return { data }
|
||||
},
|
||||
async login(email: string, _password: string): Promise<ItemResponse<MockUser>> {
|
||||
const user: MockUser = {
|
||||
id: 'user-001',
|
||||
email,
|
||||
name: 'Admin User',
|
||||
role: UserRole.ORGANIZATION_ADMIN,
|
||||
organizationId: 'org-wincasa',
|
||||
organizationName: 'Wincasa AG',
|
||||
}
|
||||
useSessionStore.getState().login(user)
|
||||
return { data: user }
|
||||
},
|
||||
async logout(): Promise<ItemResponse<void>> {
|
||||
useSessionStore.getState().logout()
|
||||
return { data: undefined }
|
||||
},
|
||||
async isAuthenticated(): Promise<ItemResponse<boolean>> {
|
||||
const data = useSessionStore.getState().isAuthenticated
|
||||
return { data }
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MockupDashboardProvider } from '../provider/MockupDashboardProvider'
|
||||
import type { DashboardStats } from '../provider/IDashboardProvider'
|
||||
import type { ItemResponse } from './types'
|
||||
|
||||
const provider = MockupDashboardProvider
|
||||
|
||||
export const dashboardService = {
|
||||
async getStats(organizationId?: string): Promise<ItemResponse<DashboardStats>> {
|
||||
const data = await provider.getStats(organizationId)
|
||||
return { data }
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MockupReviewProvider } from '../provider/MockupReviewProvider'
|
||||
import type { ReviewFilters } from '../provider/IReviewProvider'
|
||||
import type { ReviewQueueItem } from '../domain/review'
|
||||
import type { ListResponse, ItemResponse } from './types'
|
||||
|
||||
const provider = MockupReviewProvider
|
||||
|
||||
export const reviewService = {
|
||||
async getQueue(filters?: ReviewFilters): Promise<ListResponse<ReviewQueueItem>> {
|
||||
const data = await provider.getQueue(filters)
|
||||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||||
},
|
||||
async getById(id: string): Promise<ItemResponse<ReviewQueueItem | null>> {
|
||||
const data = await provider.getById(id)
|
||||
return { data }
|
||||
},
|
||||
async approve(id: string, reviewedBy: string, notes?: string): Promise<ItemResponse<ReviewQueueItem>> {
|
||||
const data = await provider.approve(id, reviewedBy, notes)
|
||||
return { data }
|
||||
},
|
||||
async reject(id: string, reviewedBy: string, notes?: string): Promise<ItemResponse<ReviewQueueItem>> {
|
||||
const data = await provider.reject(id, reviewedBy, notes)
|
||||
return { data }
|
||||
},
|
||||
async assign(id: string, assignTo: string): Promise<ItemResponse<ReviewQueueItem>> {
|
||||
const data = await provider.assign(id, assignTo)
|
||||
return { data }
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { MockupShortlistProvider } from '../provider/MockupShortlistProvider'
|
||||
import type { ShortlistFilters } from '../provider/IShortlistProvider'
|
||||
import type { Shortlist, CreateShortlistInput, UpdateShortlistInput } from '../domain/shortlist'
|
||||
import type { ListResponse, ItemResponse } from './types'
|
||||
|
||||
const provider = MockupShortlistProvider
|
||||
|
||||
export const shortlistService = {
|
||||
async getAll(filters?: ShortlistFilters): Promise<ListResponse<Shortlist>> {
|
||||
const data = await provider.getAll(filters)
|
||||
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
|
||||
},
|
||||
async getById(id: string): Promise<ItemResponse<Shortlist | null>> {
|
||||
const data = await provider.getById(id)
|
||||
return { data }
|
||||
},
|
||||
async create(input: CreateShortlistInput): Promise<ItemResponse<Shortlist>> {
|
||||
const data = await provider.create(input)
|
||||
return { data }
|
||||
},
|
||||
async update(id: string, input: UpdateShortlistInput): Promise<ItemResponse<Shortlist>> {
|
||||
const data = await provider.update(id, input)
|
||||
return { data }
|
||||
},
|
||||
async addItem(id: string, propertyId: string, note?: string): Promise<ItemResponse<Shortlist>> {
|
||||
const data = await provider.addItem(id, propertyId, note)
|
||||
return { data }
|
||||
},
|
||||
async removeItem(id: string, propertyId: string): Promise<ItemResponse<Shortlist>> {
|
||||
const data = await provider.removeItem(id, propertyId)
|
||||
return { data }
|
||||
},
|
||||
async remove(id: string): Promise<ItemResponse<void>> {
|
||||
await provider.remove(id)
|
||||
return { data: undefined }
|
||||
},
|
||||
}
|
||||
+30
-3
@@ -1,14 +1,41 @@
|
||||
export interface ServiceMeta {
|
||||
// ── Error Codes ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const ServiceErrorCode = {
|
||||
NETWORK_ERROR: 'network_error',
|
||||
UNAUTHORIZED: 'unauthorized',
|
||||
FORBIDDEN: 'forbidden',
|
||||
VALIDATION_ERROR: 'validation_error',
|
||||
NOT_FOUND: 'not_found',
|
||||
AI_GENERATION_FAILED: 'ai_generation_failed',
|
||||
BACKEND_UNAVAILABLE: 'backend_unavailable',
|
||||
} as const
|
||||
export type ServiceErrorCode = typeof ServiceErrorCode[keyof typeof ServiceErrorCode]
|
||||
|
||||
export interface ServiceError {
|
||||
code: ServiceErrorCode
|
||||
message: string
|
||||
details?: unknown
|
||||
}
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Pagination {
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
/** @deprecated Use Pagination */
|
||||
export type ServiceMeta = Pagination
|
||||
|
||||
// ── Response Shapes ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ServiceResponse<T> {
|
||||
data: T
|
||||
meta?: ServiceMeta
|
||||
error?: string | null
|
||||
meta?: Pagination
|
||||
pagination?: Pagination
|
||||
error?: ServiceError | string | null
|
||||
}
|
||||
|
||||
export type ListResponse<T> = ServiceResponse<T[]>
|
||||
|
||||
Reference in New Issue
Block a user