diff --git a/src/components/future-signals/FutureSignalCard.tsx b/src/components/future-signals/FutureSignalCard.tsx new file mode 100644 index 0000000..e24bf62 --- /dev/null +++ b/src/components/future-signals/FutureSignalCard.tsx @@ -0,0 +1,101 @@ +import { Box, Chip, LinearProgress, Typography } from '@mui/material' +import { SignalTypeBadge } from './SignalTypeBadge' +import { SensitivityBadge } from './SensitivityBadge' +import { SignalReviewStatusBadge } from './SignalReviewStatusBadge' +import { FutureSignalDisclaimer } from './FutureSignalDisclaimer' +import type { FutureSignal } from '../../domain/futureSignal' + +const SOURCE_LABELS: Record = { + PRESS: 'Presse', + CONSTRUCTION_PERMIT: 'Baubewilligung', + JOB_POSTING: 'Stelleninserat', + COMPANY_REPORT: 'Geschäftsbericht', + MARKET_DATA: 'Marktdaten', + MANUAL: 'Manuell', +} + +function probColor(p: number): string { + return p >= 0.7 ? '#1a7a4a' : p >= 0.5 ? '#d97706' : '#c0392b' +} + +interface Props { + signal: FutureSignal + isSelected: boolean + onSelect: (signal: FutureSignal) => void +} + +export function FutureSignalCard({ signal, isSelected, onSelect }: Props) { + const isConfidential = signal.sensitivityLevel === 'CONFIDENTIAL' + + return ( + onSelect(signal)} + sx={{ + p: 2, + cursor: 'pointer', + borderBottom: '1px solid #f1f5f9', + borderLeft: isSelected + ? '3px solid #1e3a5f' + : isConfidential + ? '3px solid #d97706' + : '3px solid transparent', + bgcolor: isSelected ? '#eff6ff' : isConfidential ? '#fffbeb' : 'transparent', + '&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' }, + }} + > + {/* Row 1: type + sensitivity + review status */} + + + + + + + {/* Row 2: title / company / location */} + + {signal.title ?? signal.companyName ?? signal.locationHint} + + {(signal.title || signal.companyName) && ( + + {signal.locationHint} + + )} + + {/* Row 3: probability */} + + + Wahrscheinlichkeit + + {Math.round(signal.probability * 100)}% + + + + + + {/* Row 4: meta chips */} + + + {signal.areaSqmEstimate && ( + + )} + + + + {/* Footer: mini disclaimer */} + + + ) +} diff --git a/src/components/future-signals/FutureSignalDetailPanel.tsx b/src/components/future-signals/FutureSignalDetailPanel.tsx new file mode 100644 index 0000000..7663dd9 --- /dev/null +++ b/src/components/future-signals/FutureSignalDetailPanel.tsx @@ -0,0 +1,254 @@ +import { Box, Button, Chip, CircularProgress, Divider, IconButton, LinearProgress, Paper, Snackbar, Typography } from '@mui/material' +import { X } from 'lucide-react' +import { useState } from 'react' +import { SignalTypeBadge } from './SignalTypeBadge' +import { SensitivityBadge } from './SensitivityBadge' +import { SignalReviewStatusBadge } from './SignalReviewStatusBadge' +import { FutureSignalDisclaimer } from './FutureSignalDisclaimer' +import { useUpdateSignalReviewStatus } from '../../hooks/useFutureSignals' +import { useShortlistStore } from '../../stores/shortlistStore' +import { reviewService } from '../../services/reviewService' +import { ReviewStatus } from '../../domain/enums' +import type { FutureSignal } from '../../domain/futureSignal' + +const SOURCE_LABELS: Record = { + PRESS: 'Pressebericht', + CONSTRUCTION_PERMIT: 'Baubewilligung', + JOB_POSTING: 'Stelleninserat', + COMPANY_REPORT: 'Geschäftsbericht', + MARKET_DATA: 'Marktdaten', + MANUAL: 'Manuell erfasst', +} + +const CREDIBILITY_META: Record = { + HIGH: { label: 'Hoch', color: '#1a7a4a' }, + MEDIUM: { label: 'Mittel', color: '#d97706' }, + LOW: { label: 'Niedrig', color: '#c0392b' }, +} + +function BarRow({ label, value }: { label: string; value: number }) { + const color = value >= 0.75 ? '#1a7a4a' : value >= 0.55 ? '#d97706' : '#c0392b' + return ( + + + {label} + {Math.round(value * 100)}% + + + + ) +} + +interface Props { + signal: FutureSignal + onClose: () => void +} + +export function FutureSignalDetailPanel({ signal, onClose }: Props) { + const updateStatus = useUpdateSignalReviewStatus() + const { openAddDialog } = useShortlistStore() + const [snackbar, setSnackbar] = useState(null) + const [reviewTaskSent, setReviewTaskSent] = useState(false) + + const reviewStatus = signal.reviewStatus ?? ReviewStatus.UNREVIEWED + const isRejected = reviewStatus === ReviewStatus.REJECTED + const isApproved = reviewStatus === ReviewStatus.APPROVED + + async function handleStatus(status: typeof ReviewStatus[keyof typeof ReviewStatus]) { + await updateStatus.mutateAsync({ id: signal.id, status }) + setSnackbar(`Status aktualisiert: ${status}`) + } + + async function handleSendReview() { + await reviewService.createReviewTask(signal.id) + setReviewTaskSent(true) + setSnackbar('Prüfungsaufgabe erstellt') + } + + function handleShortlist() { + openAddDialog({ + resultId: signal.id, + resultType: 'FUTURE_AVAILABILITY', + title: signal.title ?? signal.companyName ?? signal.locationHint, + matchScore: Math.round(signal.confidenceScore * 100), + confidenceScore: signal.confidenceScore, + sourceLabel: SOURCE_LABELS[signal.source.type] ?? signal.source.type, + addedBy: 'admin@ideal-sharing.ch', + }) + } + + const credMeta = CREDIBILITY_META[signal.source.credibility] ?? { label: signal.source.credibility, color: '#64748b' } + + return ( + + {/* Header */} + + + + + {signal.title ?? signal.companyName ?? signal.locationHint} + + {(signal.title || signal.companyName) && ( + {signal.locationHint} + )} + + + + + + + + + + {signal.isVerified && ( + + )} + + + + {/* Body */} + + + + {/* Probability + confidence */} + + + + + + {/* Meta */} + + + Zeithorizont + ~{signal.timeHorizonMonths} Monate + + {signal.areaSqmEstimate && ( + + Geschätzte Fläche + ~{signal.areaSqmEstimate.toLocaleString('de-CH')} m² + + )} + {signal.companyName && ( + + Unternehmen + {signal.companyName} + + )} + {signal.riskLevel && ( + + Risikoniveau + {signal.riskLevel} + + )} + + + + + {/* Source */} + Quelle + + + Typ + {SOURCE_LABELS[signal.source.type] ?? signal.source.type} + + + Glaubwürdigkeit + + + {signal.source.publishedAt && ( + + Veröffentlicht + {signal.source.publishedAt} + + )} + + + {/* Evidence */} + {signal.evidence?.summary && ( + <> + + Evidenz + + {signal.evidence.summary} + + + )} + + {/* Market indicator */} + {signal.marketIndicator && ( + <> + + Marktindikator + {signal.marketIndicator} + + )} + + + + {/* Actions */} + Aktionen + + {reviewStatus === ReviewStatus.UNREVIEWED && ( + + )} + {!isRejected && !reviewTaskSent && ( + + )} + {(reviewStatus === ReviewStatus.IN_REVIEW || reviewStatus === ReviewStatus.FLAGGED) && !isApproved && ( + + )} + {!isRejected && ( + + )} + + + + + setSnackbar(null)} + message={snackbar} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + /> + + ) +} diff --git a/src/components/future-signals/FutureSignalDisclaimer.tsx b/src/components/future-signals/FutureSignalDisclaimer.tsx new file mode 100644 index 0000000..d903120 --- /dev/null +++ b/src/components/future-signals/FutureSignalDisclaimer.tsx @@ -0,0 +1,27 @@ +import { Alert, Typography } from '@mui/material' + +interface Props { + mini?: boolean +} + +export function FutureSignalDisclaimer({ mini = false }: Props) { + if (mini) { + return ( + + Probabilistisches Signal – keine bestätigte Fläche + + ) + } + + return ( + + + Hinweis: Probabilistisches Zukunftssignal + + + Dieses Signal basiert auf AI-Analyse öffentlicher Daten. Es handelt sich um keine bestätigte verfügbare Fläche. + Bitte ausschliesslich für strategische Beobachtung und interne Prüfung verwenden — keine verbindlichen Aussagen gegenüber Dritten. + + + ) +} diff --git a/src/components/future-signals/FutureSignalEmptyState.tsx b/src/components/future-signals/FutureSignalEmptyState.tsx new file mode 100644 index 0000000..90c4615 --- /dev/null +++ b/src/components/future-signals/FutureSignalEmptyState.tsx @@ -0,0 +1,28 @@ +import { Box, Typography } from '@mui/material' +import { Filter, Radio } from 'lucide-react' + +type EmptyContext = 'no-signals' | 'filtered-empty' + +const META: Record = { + 'no-signals': { + icon: , + title: 'Keine Signale vorhanden', + desc: 'Es wurden noch keine Zukunftssignale erfasst.', + }, + 'filtered-empty': { + icon: , + title: 'Keine Signale für diese Filter', + desc: 'Passen Sie die Filtereinstellungen an, um Signale anzuzeigen.', + }, +} + +export function FutureSignalEmptyState({ context }: { context: EmptyContext }) { + const { icon, title, desc } = META[context] + return ( + + {icon} + {title} + {desc} + + ) +} diff --git a/src/components/future-signals/FutureSignalFilterBar.tsx b/src/components/future-signals/FutureSignalFilterBar.tsx new file mode 100644 index 0000000..5457bb1 --- /dev/null +++ b/src/components/future-signals/FutureSignalFilterBar.tsx @@ -0,0 +1,143 @@ +import { Box, Chip, FormControl, InputLabel, MenuItem, Select, Typography } from '@mui/material' +import { SignalType } from '../../domain/enums' +import { SIGNAL_TYPE_LABELS } from '../../lib/constants' + +export interface SignalFilterState { + signalType: string + minConfidence: number + sensitivityLevel: string + reviewStatus: string + timeHorizon: string +} + +export const DEFAULT_SIGNAL_FILTERS: SignalFilterState = { + signalType: '', + minConfidence: 0, + sensitivityLevel: '', + reviewStatus: '', + timeHorizon: '', +} + +interface Props { + filters: SignalFilterState + onChange: (f: SignalFilterState) => void + totalCount: number + filteredCount: number +} + +const SIGNAL_TYPES = Object.values(SignalType) + +export function FutureSignalFilterBar({ filters, onChange, totalCount, filteredCount }: Props) { + const set = (partial: Partial) => onChange({ ...filters, ...partial }) + + const activeCount = [ + filters.signalType !== '', + filters.minConfidence > 0, + filters.sensitivityLevel !== '', + filters.reviewStatus !== '', + filters.timeHorizon !== '', + ].filter(Boolean).length + + return ( + + {/* Signal type chips */} + + set({ signalType: '' })} + color={filters.signalType === '' ? 'primary' : 'default'} + sx={{ fontWeight: filters.signalType === '' ? 700 : 400 }} + /> + {SIGNAL_TYPES.map(t => ( + set({ signalType: filters.signalType === t ? '' : t })} + sx={{ + fontWeight: filters.signalType === t ? 700 : 400, + bgcolor: filters.signalType === t ? '#1e3a5f' : undefined, + color: filters.signalType === t ? 'white' : undefined, + }} + /> + ))} + + + {/* Selects */} + + + Konfidenz + + + + + Prüfstatus + + + + + Zeithorizont + + + + + Vertraulichkeit + + + + + {/* Result count */} + + {activeCount > 0 && ( + onChange(DEFAULT_SIGNAL_FILTERS)} + sx={{ bgcolor: '#eff6ff', color: '#1e3a5f' }} + /> + )} + + {filteredCount} / {totalCount} + + + + ) +} diff --git a/src/components/future-signals/SensitivityBadge.tsx b/src/components/future-signals/SensitivityBadge.tsx new file mode 100644 index 0000000..2ab119a --- /dev/null +++ b/src/components/future-signals/SensitivityBadge.tsx @@ -0,0 +1,19 @@ +import { Chip } from '@mui/material' + +const SENSITIVITY_META: Record = { + PUBLIC: { label: 'Öffentlich', color: '#64748b' }, + INTERNAL: { label: 'Intern', color: '#d97706' }, + CONFIDENTIAL: { label: 'Vertraulich', color: '#c0392b' }, + RESTRICTED: { label: 'Eingeschränkt', color: '#7c3aed' }, +} + +export function SensitivityBadge({ level }: { level: string }) { + const meta = SENSITIVITY_META[level] ?? { label: level, color: '#64748b' } + return ( + + ) +} diff --git a/src/components/future-signals/SignalReviewStatusBadge.tsx b/src/components/future-signals/SignalReviewStatusBadge.tsx new file mode 100644 index 0000000..d325850 --- /dev/null +++ b/src/components/future-signals/SignalReviewStatusBadge.tsx @@ -0,0 +1,22 @@ +import { Chip } from '@mui/material' +import type { ReviewStatus } from '../../domain/enums' + +const STATUS_META: Record = { + UNREVIEWED: { label: 'Ungeprüft', color: '#94a3b8' }, + IN_REVIEW: { label: 'In Prüfung', color: '#d97706' }, + APPROVED: { label: 'Genehmigt', color: '#1a7a4a' }, + REJECTED: { label: 'Abgelehnt', color: '#c0392b' }, + FLAGGED: { label: 'Markiert', color: '#ea580c' }, +} + +export function SignalReviewStatusBadge({ status }: { status?: ReviewStatus | null }) { + const key = status ?? 'UNREVIEWED' + const meta = STATUS_META[key] ?? { label: key, color: '#94a3b8' } + return ( + + ) +} diff --git a/src/components/future-signals/index.ts b/src/components/future-signals/index.ts index 8f289fa..2e07a41 100644 --- a/src/components/future-signals/index.ts +++ b/src/components/future-signals/index.ts @@ -1 +1,10 @@ export { SignalTypeBadge } from './SignalTypeBadge' +export { FutureSignalDisclaimer } from './FutureSignalDisclaimer' +export { SensitivityBadge } from './SensitivityBadge' +export { SignalReviewStatusBadge } from './SignalReviewStatusBadge' +export { FutureSignalCard } from './FutureSignalCard' +export { FutureSignalFilterBar } from './FutureSignalFilterBar' +export { FutureSignalDetailPanel } from './FutureSignalDetailPanel' +export { FutureSignalEmptyState } from './FutureSignalEmptyState' +export type { SignalFilterState } from './FutureSignalFilterBar' +export { DEFAULT_SIGNAL_FILTERS } from './FutureSignalFilterBar' diff --git a/src/hooks/useFutureSignals.ts b/src/hooks/useFutureSignals.ts index 0ed6829..ceec3d2 100644 --- a/src/hooks/useFutureSignals.ts +++ b/src/hooks/useFutureSignals.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { futureSignalService } from '../services/futureSignalService' +import type { ReviewStatus } from '../domain/enums' import { STALE_SIGNALS } from '../lib/constants' export function useFutureSignals() { @@ -11,6 +12,16 @@ export function useFutureSignals() { }) } +export function useFutureSignal(id: string) { + return useQuery({ + queryKey: ['futureSignal', id], + queryFn: () => futureSignalService.getById(id), + staleTime: STALE_SIGNALS, + enabled: !!id, + select: (res) => res.data ?? null, + }) +} + export function useFutureSignalsByProperty(propertyId: string) { return useQuery({ queryKey: ['futureSignals', 'property', propertyId], @@ -24,9 +35,21 @@ export function useFutureSignalsByProperty(propertyId: string) { export function useVerifySignal() { const queryClient = useQueryClient() return useMutation({ - mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'current-user'), + mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'admin@ideal-sharing.ch'), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['futureSignals'] }) }, }) } + +export function useUpdateSignalReviewStatus() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: ReviewStatus }) => + futureSignalService.updateReviewStatus(id, status), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['futureSignals'] }) + queryClient.invalidateQueries({ queryKey: ['futureSignal'] }) + }, + }) +} diff --git a/src/pages/supply/FutureAvailability.tsx b/src/pages/supply/FutureAvailability.tsx index c76d134..f55c7a5 100644 --- a/src/pages/supply/FutureAvailability.tsx +++ b/src/pages/supply/FutureAvailability.tsx @@ -1,234 +1,141 @@ +import { useState } from 'react' +import { Box, Chip, Paper, Typography } from '@mui/material' +import { useFutureSignals } from '../../hooks/useFutureSignals' import { - Box, - Button, - Card, - Chip, - Divider, - LinearProgress, - Typography, -} from '@mui/material' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { EmptyState, ErrorState, LoadingPage } from '../../components/ui' -import { futureSignalService } from '../../services/futureSignalService' -import { SignalType } from '../../domain/enums' + FutureSignalCard, + FutureSignalFilterBar, + FutureSignalDetailPanel, + FutureSignalEmptyState, + DEFAULT_SIGNAL_FILTERS, +} from '../../components/future-signals' +import { AddToShortlistDialog } from '../../components/shortlist' +import type { SignalFilterState } from '../../components/future-signals' +import type { FutureSignal } from '../../domain/futureSignal' -function getSignalTypeLabel(type: SignalType): string { - switch (type) { - case SignalType.EXPANSION: return 'Expansion' - case SignalType.POSSIBLE_MOVE_OUT: return 'Möglicher Auszug' - case SignalType.CONSTRUCTION_PROJECT: return 'Bauprojekt' - case SignalType.RESTRUCTURING: return 'Restrukturierung' - case SignalType.PROJECT_DEVELOPMENT: return 'Projektentwicklung' - case SignalType.SPACE_CONSOLIDATION: return 'Flächenkonsolidierung' - } -} - -function getSignalTypeColor(type: SignalType): string { - switch (type) { - case SignalType.EXPANSION: return '#1a7a4a' - case SignalType.POSSIBLE_MOVE_OUT: return '#d97706' - case SignalType.CONSTRUCTION_PROJECT: return '#1e3a5f' - case SignalType.RESTRUCTURING: return '#ea580c' - case SignalType.PROJECT_DEVELOPMENT: return '#7c3aed' - case SignalType.SPACE_CONSOLIDATION: return '#6b7280' - } -} - -function getProbabilityColor(prob: number): 'success' | 'warning' | 'error' { - if (prob > 0.7) return 'success' - if (prob >= 0.5) return 'warning' - return 'error' -} - -function formatDate(dateStr?: string): string { - if (!dateStr) return '–' - return new Date(dateStr).toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }) -} - -function getSourceTypeLabel(type: string): string { - switch (type) { - case 'PRESS': return 'Pressebericht' - case 'CONSTRUCTION_PERMIT': return 'Baubewilligung' - case 'JOB_POSTING': return 'Stelleninserat' - case 'COMPANY_REPORT': return 'Geschäftsbericht' - case 'MARKET_DATA': return 'Marktdaten' - case 'MANUAL': return 'Manuell' - default: return type - } +function applyFilters(signals: FutureSignal[], f: SignalFilterState): FutureSignal[] { + return signals.filter(s => { + if (f.signalType && s.signalType !== f.signalType) return false + if (f.minConfidence > 0 && s.confidenceScore < f.minConfidence) return false + if (f.sensitivityLevel && s.sensitivityLevel !== f.sensitivityLevel) return false + if (f.reviewStatus) { + const rs = s.reviewStatus ?? 'UNREVIEWED' + if (rs !== f.reviewStatus) return false + } + if (f.timeHorizon === 'short' && s.timeHorizonMonths > 6) return false + if (f.timeHorizon === 'medium' && (s.timeHorizonMonths <= 6 || s.timeHorizonMonths > 12)) return false + if (f.timeHorizon === 'long' && s.timeHorizonMonths <= 12) return false + return true + }) } export default function FutureAvailability() { - const queryClient = useQueryClient() + const { data: signals = [], isLoading } = useFutureSignals() + const [filters, setFilters] = useState(DEFAULT_SIGNAL_FILTERS) + const [selectedSignal, setSelectedSignal] = useState(null) - const { data: resp, isLoading, error } = useQuery({ - queryKey: ['futureSignals'], - queryFn: () => futureSignalService.getAll(), - }) + const filtered = applyFilters(signals, filters) - const verifyMutation = useMutation({ - mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'admin@ideal-sharing.ch'), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['futureSignals'] }), - }) + const needsReviewCount = signals.filter(s => !s.reviewStatus || s.reviewStatus === 'UNREVIEWED').length + const highConfCount = signals.filter(s => s.confidenceScore >= 0.75).length + const confidentialCount = signals.filter(s => s.sensitivityLevel === 'CONFIDENTIAL').length - if (isLoading) return - if (error) return - - const signals = resp?.data ?? [] - - const totalCount = signals.length - const verifiedCount = signals.filter(s => s.isVerified).length - const highProbCount = signals.filter(s => s.probability > 0.7).length - const avgProbability = signals.length - ? signals.reduce((sum, s) => sum + s.probability, 0) / signals.length - : 0 + function handleSelect(signal: FutureSignal) { + setSelectedSignal(prev => prev?.id === signal.id ? null : signal) + } return ( - - {/* Page Header */} - - - - Marktchancen - + + + + {/* Header */} + + + + Zukunftssignale + Probabilistische Markt- und Verfügbarkeitssignale - KI-generierte Verfügbarkeitssignale + + + + + 0 ? '#fef3c7' : '#f1f5f9', color: needsReviewCount > 0 ? '#d97706' : '#475569', fontWeight: needsReviewCount > 0 ? 600 : 400 }} + /> + + {confidentialCount > 0 && ( + + )} - {/* Content */} - + {/* Filter bar */} + - {/* Stats Row */} - - - Signale gesamt - {totalCount} - - - Verifiziert - {verifiedCount} - - - Hohe Wahrscheinlichkeit - {highProbCount} - - - Ø Wahrscheinlichkeit - - {Math.round(avgProbability * 100)}% - - + {/* Body */} + + {/* Signal list */} + + {isLoading ? ( + + {[0, 1, 2, 3].map(i => ( + + ))} + + ) : filtered.length === 0 ? ( + + ) : ( + + {filtered.map(signal => ( + + ))} + + )} - {/* Signal Cards Grid */} - {signals.length === 0 ? ( - - ) : ( - - {signals.map(signal => ( - - {/* Card Header */} - - - - - - {/* Location */} - - {signal.locationHint} - {signal.companyName && ( - {signal.companyName} - )} - - - {/* Probability */} - - - Wahrscheinlichkeit - 0.7 ? '#1a7a4a' : signal.probability >= 0.5 ? '#d97706' : '#c0392b' }}> - {Math.round(signal.probability * 100)}% - - - - - - {/* Details */} - - {signal.areaSqmEstimate && ( - - Fläche: ca. {signal.areaSqmEstimate.toLocaleString('de-CH')} m² - - )} - - Zeithorizont: {signal.timeHorizonMonths} Monate - - {signal.expiresAt && ( - - Verfügbar ab: {formatDate(signal.expiresAt)} - - )} - - - {/* Source */} - - - Quelle: {getSourceTypeLabel(signal.source.type)} — Glaubwürdigkeit:{' '} - - {signal.source.credibility === 'HIGH' ? 'Hoch' : signal.source.credibility === 'MEDIUM' ? 'Mittel' : 'Niedrig'} - - - - - {/* Verification Status */} - - {signal.isVerified ? ( - - - {formatDate(signal.verifiedAt)} - - ) : ( - - )} - - - - - {/* Footer buttons */} - - {!signal.isVerified && ( - - )} - - - - ))} - + {/* Detail panel */} + {selectedSignal && ( + + setSelectedSignal(null)} + /> + )} diff --git a/src/provider/IFutureSignalProvider.ts b/src/provider/IFutureSignalProvider.ts index 81f93f5..57a8c31 100644 --- a/src/provider/IFutureSignalProvider.ts +++ b/src/provider/IFutureSignalProvider.ts @@ -1,11 +1,14 @@ import type { FutureSignal } from '../domain/futureSignal' -import type { SignalType } from '../domain/enums' +import type { SignalType, ReviewStatus } from '../domain/enums' export interface FutureSignalFilters { signalType?: SignalType minProbability?: number organizationId?: string isVerified?: boolean + sensitivityLevel?: string + reviewStatus?: ReviewStatus + maxTimeHorizonMonths?: number } export interface IFutureSignalProvider { @@ -13,4 +16,5 @@ export interface IFutureSignalProvider { getById(id: string): Promise getByProperty(propertyId: string): Promise verify(id: string, verifiedBy: string): Promise + updateReviewStatus(id: string, status: ReviewStatus): Promise } diff --git a/src/provider/MockupFutureSignalProvider.ts b/src/provider/MockupFutureSignalProvider.ts index 5755e28..1143780 100644 --- a/src/provider/MockupFutureSignalProvider.ts +++ b/src/provider/MockupFutureSignalProvider.ts @@ -1,5 +1,6 @@ import type { IFutureSignalProvider, FutureSignalFilters } from './IFutureSignalProvider' import type { FutureSignal } from '../domain/futureSignal' +import type { ReviewStatus } from '../domain/enums' import { mockFutureSignals } from '../mock-data/futureSignals' const store: FutureSignal[] = [...mockFutureSignals] @@ -11,6 +12,9 @@ export const MockupFutureSignalProvider: IFutureSignalProvider = { if (filters?.minProbability !== undefined) results = results.filter(s => s.probability >= filters.minProbability!) if (filters?.organizationId) results = results.filter(s => s.organizationId === filters.organizationId) if (filters?.isVerified !== undefined) results = results.filter(s => s.isVerified === filters.isVerified) + if (filters?.sensitivityLevel) results = results.filter(s => s.sensitivityLevel === filters.sensitivityLevel) + if (filters?.reviewStatus) results = results.filter(s => (s.reviewStatus ?? 'UNREVIEWED') === filters.reviewStatus) + if (filters?.maxTimeHorizonMonths !== undefined) results = results.filter(s => s.timeHorizonMonths <= filters.maxTimeHorizonMonths!) return results.sort((a, b) => b.probability - a.probability) }, async getById(id) { @@ -24,4 +28,9 @@ export const MockupFutureSignalProvider: IFutureSignalProvider = { store[idx] = { ...store[idx], isVerified: true, verifiedBy, verifiedAt: new Date().toISOString(), updatedAt: new Date().toISOString() } return store[idx] }, + async updateReviewStatus(id, status: ReviewStatus) { + const idx = store.findIndex(s => s.id === id) + store[idx] = { ...store[idx], reviewStatus: status, updatedAt: new Date().toISOString() } + return store[idx] + }, } diff --git a/src/services/futureSignalService.ts b/src/services/futureSignalService.ts index 0ecb410..3e9d43f 100644 --- a/src/services/futureSignalService.ts +++ b/src/services/futureSignalService.ts @@ -1,6 +1,7 @@ import { MockupFutureSignalProvider } from '../provider/MockupFutureSignalProvider' import type { FutureSignalFilters } from '../provider/IFutureSignalProvider' import type { FutureSignal } from '../domain/futureSignal' +import type { ReviewStatus } from '../domain/enums' import type { FutureSignalSummary } from '../domain/dashboard' import type { ListResponse, ItemResponse } from './types' @@ -23,6 +24,10 @@ export const futureSignalService = { const data = await provider.verify(id, verifiedBy) return { data } }, + async updateReviewStatus(id: string, status: ReviewStatus): Promise> { + const data = await provider.updateReviewStatus(id, status) + return { data } + }, async getSignalsForProperty(propertyId: string): Promise> { const data = await provider.getByProperty(propertyId)