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:
@@ -6,3 +6,5 @@ export * from './futureSignal'
|
||||
export * from './aiOutput'
|
||||
export * from './activityEvent'
|
||||
export * from './unifiedResult'
|
||||
export * from './shortlist'
|
||||
export * from './review'
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export const ReviewPriority = {
|
||||
HIGH: 'HIGH',
|
||||
MEDIUM: 'MEDIUM',
|
||||
LOW: 'LOW',
|
||||
} as const
|
||||
export type ReviewPriority = typeof ReviewPriority[keyof typeof ReviewPriority]
|
||||
|
||||
export const ReviewQueueStatus = {
|
||||
PENDING: 'PENDING',
|
||||
IN_REVIEW: 'IN_REVIEW',
|
||||
COMPLETED: 'COMPLETED',
|
||||
} as const
|
||||
export type ReviewQueueStatus = typeof ReviewQueueStatus[keyof typeof ReviewQueueStatus]
|
||||
|
||||
export interface ReviewQueueItem {
|
||||
id: string
|
||||
matchId: string
|
||||
needId: string
|
||||
propertyId: string
|
||||
matchScore: number
|
||||
priority: ReviewPriority
|
||||
status: ReviewQueueStatus
|
||||
assignedTo?: string
|
||||
notes?: string
|
||||
dueAt?: string
|
||||
organizationId?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ShortlistStatus } from './enums'
|
||||
|
||||
export interface ShortlistItem {
|
||||
propertyId: string
|
||||
addedAt: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
export interface Shortlist {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
needId?: string
|
||||
items: ShortlistItem[]
|
||||
status: ShortlistStatus
|
||||
createdBy: string
|
||||
organizationId?: string
|
||||
sharedWith?: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type CreateShortlistInput = Omit<Shortlist, 'id' | 'createdAt' | 'updatedAt'>
|
||||
export type UpdateShortlistInput = Partial<CreateShortlistInput>
|
||||
+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 }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const mockDelay = (ms = 150) => new Promise<void>(res => setTimeout(res, ms))
|
||||
@@ -2,3 +2,5 @@ export { mockProperties } from './properties'
|
||||
export { mockNeeds } from './needs'
|
||||
export { mockMatches } from './matches'
|
||||
export { mockFutureSignals } from './futureSignals'
|
||||
export { mockShortlists } from './shortlists'
|
||||
export { mockReviewQueue } from './reviewQueue'
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ReviewQueueItem } from '../domain/review'
|
||||
|
||||
export const mockReviewQueue: ReviewQueueItem[] = [
|
||||
{
|
||||
id: 'review-001',
|
||||
matchId: 'match-001',
|
||||
needId: 'need-001',
|
||||
propertyId: 'prop-001',
|
||||
matchScore: 88,
|
||||
priority: 'HIGH',
|
||||
status: 'PENDING',
|
||||
assignedTo: 'user-001',
|
||||
dueAt: '2025-06-15T17:00:00Z',
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-05-10T08:00:00Z',
|
||||
updatedAt: '2025-05-10T08:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'review-002',
|
||||
matchId: 'match-002',
|
||||
needId: 'need-001',
|
||||
propertyId: 'prop-004',
|
||||
matchScore: 64,
|
||||
priority: 'MEDIUM',
|
||||
status: 'PENDING',
|
||||
dueAt: '2025-06-20T17:00:00Z',
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-05-10T08:15:00Z',
|
||||
updatedAt: '2025-05-10T08:15:00Z',
|
||||
},
|
||||
{
|
||||
id: 'review-003',
|
||||
matchId: 'match-004',
|
||||
needId: 'need-002',
|
||||
propertyId: 'prop-006',
|
||||
matchScore: 47,
|
||||
priority: 'LOW',
|
||||
status: 'PENDING',
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-05-11T09:00:00Z',
|
||||
updatedAt: '2025-05-11T09:00:00Z',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Shortlist } from '../domain/shortlist'
|
||||
import { ShortlistStatus } from '../domain/enums'
|
||||
|
||||
export const mockShortlists: Shortlist[] = [
|
||||
{
|
||||
id: 'shortlist-001',
|
||||
title: 'Top Büroflächen Zürich',
|
||||
description: 'Beste Bürooptionen für Innovatech AG',
|
||||
needId: 'need-001',
|
||||
items: [
|
||||
{ propertyId: 'prop-001', addedAt: '2025-03-01T10:00:00Z', note: 'Erste Wahl' },
|
||||
{ propertyId: 'prop-004', addedAt: '2025-03-02T14:30:00Z', note: 'Interessante Alternative' },
|
||||
],
|
||||
status: ShortlistStatus.ACTIVE,
|
||||
createdBy: 'user-001',
|
||||
organizationId: 'org-wincasa',
|
||||
createdAt: '2025-03-01T10:00:00Z',
|
||||
updatedAt: '2025-03-02T14:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 'shortlist-002',
|
||||
title: 'Logistik Optionen Basel',
|
||||
description: 'Auswahl für Schweizer Logistik GmbH',
|
||||
needId: 'need-002',
|
||||
items: [
|
||||
{ propertyId: 'prop-002', addedAt: '2025-03-05T09:00:00Z', note: 'Perfekte Grösse' },
|
||||
],
|
||||
status: ShortlistStatus.SHARED,
|
||||
createdBy: 'user-001',
|
||||
organizationId: 'org-wincasa',
|
||||
sharedWith: ['client@schweizer-logistik.ch'],
|
||||
createdAt: '2025-03-05T09:00:00Z',
|
||||
updatedAt: '2025-03-06T11:00:00Z',
|
||||
},
|
||||
]
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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