From 3b45b0c121a9e618578005e665bf78d28954c67e Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Fri, 15 May 2026 10:50:26 +0200 Subject: [PATCH] feat(F020): frontend infrastructure foundation - src/lib/constants.ts: centralized route paths, labels, thresholds - src/lib/utils.ts: formatting, color helpers, math utils - src/hooks/: useProperties, useMatches, useFutureSignals, useNeeds - src/components/scores/MatchScoreRing - src/components/data-quality/DataQualityBar - src/components/future-signals/SignalTypeBadge - src/components/cards/SourceTypeBadge - src/provider/AuthProvider (placeholder, swappable) - main.tsx: AuthProvider + centralized stale time Co-Authored-By: Claude Sonnet 4.6 --- src/components/cards/SourceTypeBadge.tsx | 27 +++++ src/components/cards/index.ts | 1 + .../data-quality/DataQualityBar.tsx | 101 ++++++++++++++++ src/components/data-quality/index.ts | 1 + .../future-signals/SignalTypeBadge.tsx | 28 +++++ src/components/future-signals/index.ts | 1 + src/components/scores/MatchScoreRing.tsx | 57 +++++++++ src/components/scores/index.ts | 1 + src/hooks/index.ts | 4 + src/hooks/useFutureSignals.ts | 32 +++++ src/hooks/useMatches.ts | 42 +++++++ src/hooks/useNeeds.ts | 19 +++ src/hooks/useProperties.ts | 32 +++++ src/lib/constants.ts | 113 ++++++++++++++++++ src/lib/utils.ts | 99 +++++++++++++++ src/main.tsx | 8 +- src/provider/AuthProvider.tsx | 36 ++++++ 17 files changed, 600 insertions(+), 2 deletions(-) create mode 100644 src/components/cards/SourceTypeBadge.tsx create mode 100644 src/components/cards/index.ts create mode 100644 src/components/data-quality/DataQualityBar.tsx create mode 100644 src/components/data-quality/index.ts create mode 100644 src/components/future-signals/SignalTypeBadge.tsx create mode 100644 src/components/future-signals/index.ts create mode 100644 src/components/scores/MatchScoreRing.tsx create mode 100644 src/components/scores/index.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useFutureSignals.ts create mode 100644 src/hooks/useMatches.ts create mode 100644 src/hooks/useNeeds.ts create mode 100644 src/hooks/useProperties.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/utils.ts create mode 100644 src/provider/AuthProvider.tsx diff --git a/src/components/cards/SourceTypeBadge.tsx b/src/components/cards/SourceTypeBadge.tsx new file mode 100644 index 0000000..ca92967 --- /dev/null +++ b/src/components/cards/SourceTypeBadge.tsx @@ -0,0 +1,27 @@ +import { Chip } from '@mui/material' +import { ShieldCheck, Globe, Sparkles } from 'lucide-react' +import type { ResultType } from '../../domain/enums' +import { RESULT_TYPE_LABELS } from '../../lib/constants' + +interface SourceTypeBadgeProps { + type: ResultType + size?: 'small' | 'medium' +} + +const CONFIG: Record = { + VERIFIED_PORTFOLIO: { bg: 'rgba(30,58,95,0.1)', color: '#1e3a5f', Icon: ShieldCheck }, + EXTERNAL_MARKET: { bg: 'rgba(217,119,6,0.1)', color: '#b45309', Icon: Globe }, + FUTURE_AVAILABILITY: { bg: 'rgba(124,58,237,0.1)', color: '#6d28d9', Icon: Sparkles }, +} + +export function SourceTypeBadge({ type, size = 'small' }: SourceTypeBadgeProps) { + const { bg, color, Icon } = CONFIG[type] ?? { bg: '#f1f5f9', color: '#475569', Icon: Globe } + return ( + } + label={RESULT_TYPE_LABELS[type] ?? type} + size={size} + sx={{ bgcolor: bg, color, fontWeight: 600, border: 'none', '& .MuiChip-icon': { ml: 0.5 } }} + /> + ) +} diff --git a/src/components/cards/index.ts b/src/components/cards/index.ts new file mode 100644 index 0000000..077e29e --- /dev/null +++ b/src/components/cards/index.ts @@ -0,0 +1 @@ +export { SourceTypeBadge } from './SourceTypeBadge' diff --git a/src/components/data-quality/DataQualityBar.tsx b/src/components/data-quality/DataQualityBar.tsx new file mode 100644 index 0000000..b4bb345 --- /dev/null +++ b/src/components/data-quality/DataQualityBar.tsx @@ -0,0 +1,101 @@ +import { Box, LinearProgress, Tooltip, Typography, Chip } from '@mui/material' +import { AlertTriangle } from 'lucide-react' +import { dataQualityColor, dataQualityHex, formatPercent } from '../../lib/utils' +import { FRESHNESS_LABELS } from '../../lib/constants' +import type { DataQuality } from '../../domain/property' + +interface DataQualityBarProps { + quality: DataQuality + compact?: boolean + showWarnings?: boolean +} + +export function DataQualityBar({ quality, compact = false, showWarnings = true }: DataQualityBarProps) { + const color = dataQualityColor(quality.score) + const hex = dataQualityHex(quality.score) + const pct = Math.round(quality.score * 100) + + const tooltipContent = ( + + + Datenqualität {formatPercent(quality.score)} + + {quality.missingCriticalFields.length > 0 && ( + + Fehlende Pflichtfelder: + {quality.missingCriticalFields.map(f => ( + • {f} + ))} + + )} + {quality.warnings.length > 0 && ( + + Warnungen: + {quality.warnings.map((w, i) => ( + • {w} + ))} + + )} + + Aktualität: {FRESHNESS_LABELS[quality.freshness]} + {quality.lastVerifiedAt ? ` · Geprüft: ${quality.lastVerifiedAt}` : ''} + + + ) + + if (compact) { + return ( + + + + + + + {pct}% + + {quality.missingCriticalFields.length > 0 && showWarnings && ( + + )} + + + ) + } + + return ( + + + + + + + + {pct}% + + + + {showWarnings && quality.missingCriticalFields.length > 0 && ( + + {quality.missingCriticalFields.slice(0, 2).map(f => ( + + ))} + {quality.missingCriticalFields.length > 2 && ( + + +{quality.missingCriticalFields.length - 2} + + )} + + )} + + ) +} diff --git a/src/components/data-quality/index.ts b/src/components/data-quality/index.ts new file mode 100644 index 0000000..b75e201 --- /dev/null +++ b/src/components/data-quality/index.ts @@ -0,0 +1 @@ +export { DataQualityBar } from './DataQualityBar' diff --git a/src/components/future-signals/SignalTypeBadge.tsx b/src/components/future-signals/SignalTypeBadge.tsx new file mode 100644 index 0000000..8022b01 --- /dev/null +++ b/src/components/future-signals/SignalTypeBadge.tsx @@ -0,0 +1,28 @@ +import { Chip } from '@mui/material' +import type { SignalType } from '../../domain/enums' +import { SIGNAL_TYPE_LABELS } from '../../lib/constants' + +interface SignalTypeBadgeProps { + type: SignalType + size?: 'small' | 'medium' +} + +const COLOR_MAP: Record = { + EXPANSION: { bg: 'rgba(26,122,74,0.12)', color: '#1a7a4a' }, + POSSIBLE_MOVE_OUT: { bg: 'rgba(217,119,6,0.12)', color: '#d97706' }, + CONSTRUCTION_PROJECT: { bg: 'rgba(37,99,235,0.12)', color: '#1d4ed8' }, + RESTRUCTURING: { bg: 'rgba(234,88,12,0.12)', color: '#c2410c' }, + PROJECT_DEVELOPMENT: { bg: 'rgba(124,58,237,0.12)', color: '#6d28d9' }, + SPACE_CONSOLIDATION: { bg: 'rgba(100,116,139,0.12)',color: '#475569' }, +} + +export function SignalTypeBadge({ type, size = 'small' }: SignalTypeBadgeProps) { + const { bg, color } = COLOR_MAP[type] ?? { bg: '#f1f5f9', color: '#475569' } + return ( + + ) +} diff --git a/src/components/future-signals/index.ts b/src/components/future-signals/index.ts new file mode 100644 index 0000000..8f289fa --- /dev/null +++ b/src/components/future-signals/index.ts @@ -0,0 +1 @@ +export { SignalTypeBadge } from './SignalTypeBadge' diff --git a/src/components/scores/MatchScoreRing.tsx b/src/components/scores/MatchScoreRing.tsx new file mode 100644 index 0000000..ebb74d0 --- /dev/null +++ b/src/components/scores/MatchScoreRing.tsx @@ -0,0 +1,57 @@ +import { Box, Typography } from '@mui/material' +import { matchScoreColor } from '../../lib/utils' +import type { MatchStrength } from '../../domain/enums' +import { MATCH_STRENGTH_LABELS } from '../../lib/constants' + +interface MatchScoreRingProps { + score: number + strength: MatchStrength + size?: 'sm' | 'md' | 'lg' + showLabel?: boolean +} + +const SIZE_MAP = { + sm: { ring: 48, font: '0.875rem', label: '0.625rem' }, + md: { ring: 64, font: '1.125rem', label: '0.75rem' }, + lg: { ring: 80, font: '1.5rem', label: '0.8125rem' }, +} + +const COLOR_MAP = { + success: '#1a7a4a', + warning: '#d97706', + error: '#c0392b', +} + +export function MatchScoreRing({ score, strength, size = 'md', showLabel = true }: MatchScoreRingProps) { + const dim = SIZE_MAP[size] + const colorKey = matchScoreColor(score) + const color = COLOR_MAP[colorKey] + const bgColor = colorKey === 'success' ? 'rgba(26,122,74,0.08)' : colorKey === 'warning' ? 'rgba(217,119,6,0.08)' : 'rgba(192,57,43,0.08)' + + return ( + + + + {score} + + + {showLabel && ( + + {MATCH_STRENGTH_LABELS[strength]} + + )} + + ) +} diff --git a/src/components/scores/index.ts b/src/components/scores/index.ts new file mode 100644 index 0000000..61bd9bc --- /dev/null +++ b/src/components/scores/index.ts @@ -0,0 +1 @@ +export { MatchScoreRing } from './MatchScoreRing' diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..1f41aa8 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,4 @@ +export { useProperties, useProperty } from './useProperties' +export { useMatches, useMatchesByNeed, useMatchesByProperty, useApproveMatch } from './useMatches' +export { useFutureSignals, useFutureSignalsByProperty, useVerifySignal } from './useFutureSignals' +export { useNeeds, useNeed } from './useNeeds' diff --git a/src/hooks/useFutureSignals.ts b/src/hooks/useFutureSignals.ts new file mode 100644 index 0000000..0ed6829 --- /dev/null +++ b/src/hooks/useFutureSignals.ts @@ -0,0 +1,32 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { futureSignalService } from '../services/futureSignalService' +import { STALE_SIGNALS } from '../lib/constants' + +export function useFutureSignals() { + return useQuery({ + queryKey: ['futureSignals'], + queryFn: () => futureSignalService.getAll(), + staleTime: STALE_SIGNALS, + select: (res) => res.data ?? [], + }) +} + +export function useFutureSignalsByProperty(propertyId: string) { + return useQuery({ + queryKey: ['futureSignals', 'property', propertyId], + queryFn: () => futureSignalService.getByProperty(propertyId), + staleTime: STALE_SIGNALS, + enabled: Boolean(propertyId), + select: (res) => res.data ?? [], + }) +} + +export function useVerifySignal() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (signalId: string) => futureSignalService.verify(signalId, 'current-user'), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['futureSignals'] }) + }, + }) +} diff --git a/src/hooks/useMatches.ts b/src/hooks/useMatches.ts new file mode 100644 index 0000000..6889860 --- /dev/null +++ b/src/hooks/useMatches.ts @@ -0,0 +1,42 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { matchService } from '../services/matchService' +import { STALE_MATCHES } from '../lib/constants' + +export function useMatches() { + return useQuery({ + queryKey: ['matches'], + queryFn: () => matchService.getAll(), + staleTime: STALE_MATCHES, + select: (res) => res.data ?? [], + }) +} + +export function useMatchesByNeed(needId: string) { + return useQuery({ + queryKey: ['matches', 'need', needId], + queryFn: () => matchService.getByNeed(needId), + staleTime: STALE_MATCHES, + enabled: Boolean(needId), + select: (res) => res.data ?? [], + }) +} + +export function useMatchesByProperty(propertyId: string) { + return useQuery({ + queryKey: ['matches', 'property', propertyId], + queryFn: () => matchService.getByProperty(propertyId), + staleTime: STALE_MATCHES, + enabled: Boolean(propertyId), + select: (res) => res.data ?? [], + }) +} + +export function useApproveMatch() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (matchId: string) => matchService.approve(matchId, 'current-user'), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['matches'] }) + }, + }) +} diff --git a/src/hooks/useNeeds.ts b/src/hooks/useNeeds.ts new file mode 100644 index 0000000..efa3fad --- /dev/null +++ b/src/hooks/useNeeds.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query' +import { needService } from '../services/needService' + +export function useNeeds() { + return useQuery({ + queryKey: ['needs'], + queryFn: () => needService.getAll(), + select: (res) => res.data ?? [], + }) +} + +export function useNeed(id: string) { + return useQuery({ + queryKey: ['need', id], + queryFn: () => needService.getById(id), + enabled: Boolean(id), + select: (res) => res.data ?? null, + }) +} diff --git a/src/hooks/useProperties.ts b/src/hooks/useProperties.ts new file mode 100644 index 0000000..997720d --- /dev/null +++ b/src/hooks/useProperties.ts @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query' +import { propertyService } from '../services/propertyService' +import type { AssetType, ResultType } from '../domain/enums' +import { STALE_PROPERTIES } from '../lib/constants' + +interface PropertyFilter { + assetType?: AssetType + resultType?: ResultType + city?: string + minAreaSqm?: number + maxRentPerSqm?: number + organizationId?: string +} + +export function useProperties(filter?: PropertyFilter) { + return useQuery({ + queryKey: ['properties', filter ?? {}], + queryFn: () => propertyService.getAll(filter), + staleTime: STALE_PROPERTIES, + select: (res) => res.data ?? [], + }) +} + +export function useProperty(id: string) { + return useQuery({ + queryKey: ['property', id], + queryFn: () => propertyService.getById(id), + staleTime: STALE_PROPERTIES, + enabled: Boolean(id), + select: (res) => res.data ?? null, + }) +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..17553fd --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,113 @@ +// Centralized app-wide constants — no magic strings anywhere else + +export const APP_NAME = 'Property-Match' +export const APP_VERSION = '0.1.0' + +// Organisation +export const DEFAULT_ORG_ID = 'org-wincasa' + +// Pagination +export const DEFAULT_PAGE_SIZE = 25 +export const MAX_COMPARE_ITEMS = 3 + +// Data quality thresholds +export const DQ_HIGH = 0.8 +export const DQ_MEDIUM = 0.6 + +// Confidence thresholds +export const CONF_HIGH = 0.85 +export const CONF_MEDIUM = 0.65 + +// Match score thresholds (0–100) +export const SCORE_STRONG = 80 +export const SCORE_MODERATE = 60 + +// Probability thresholds (future signals) +export const PROB_HIGH = 0.7 +export const PROB_MEDIUM = 0.5 + +// Query stale times (ms) +export const STALE_PROPERTIES = 5 * 60 * 1000 +export const STALE_MATCHES = 2 * 60 * 1000 +export const STALE_SIGNALS = 5 * 60 * 1000 + +// Route paths — single source of truth +export const ROUTES = { + HOME: '/', + SUPPLY: { + DASHBOARD: '/supply/dashboard', + PROPERTIES: '/supply/properties', + MATCH_CENTER: '/supply/match-center', + FUTURE_AVAILABILITY: '/supply/future-availability', + DATA_QUALITY: '/supply/data-quality', + }, + DEMAND: { + AI_SEARCH: '/demand/ai-search', + RESULTS: '/demand/results', + COMPARE: '/demand/compare', + SHORTLISTS: '/demand/shortlists', + }, + OPS: { + REVIEW_QUEUE: '/ops/review-queue', + AI_MONITORING: '/ops/ai-monitoring', + GOVERNANCE: '/ops/governance', + }, +} as const + +// Asset type display labels +export const ASSET_TYPE_LABELS: Record = { + OFFICE: 'Büro', + RETAIL: 'Retail', + GASTRO: 'Gastronomie', + LOGISTICS: 'Logistik', + PRODUCTION: 'Produktion', + MIXED: 'Gemischt', +} + +// Result type display labels +export const RESULT_TYPE_LABELS: Record = { + VERIFIED_PORTFOLIO: 'Verified Portfolio', + EXTERNAL_MARKET: 'Marktinserat', + FUTURE_AVAILABILITY: 'Zukunftssignal', +} + +// Match strength display labels +export const MATCH_STRENGTH_LABELS: Record = { + STRONG: 'Stark', + MODERATE: 'Mittel', + WEAK: 'Schwach', +} + +// Availability status display labels +export const AVAILABILITY_LABELS: Record = { + AVAILABLE_NOW: 'Verfügbar', + AVAILABLE_SOON: 'Bald verfügbar', + FUTURE_SIGNAL: 'Zukunftssignal', + OCCUPIED: 'Belegt', + UNKNOWN: 'Unbekannt', +} + +// Risk level display labels +export const RISK_LABELS: Record = { + LOW: 'Niedrig', + MEDIUM: 'Mittel', + HIGH: 'Hoch', + CRITICAL: 'Kritisch', +} + +// Signal type display labels +export const SIGNAL_TYPE_LABELS: Record = { + EXPANSION: 'Expansion', + POSSIBLE_MOVE_OUT: 'Möglicher Auszug', + CONSTRUCTION_PROJECT: 'Bauprojekt', + RESTRUCTURING: 'Umstrukturierung', + PROJECT_DEVELOPMENT: 'Projektentwicklung', + SPACE_CONSOLIDATION: 'Flächenkonsolidierung', +} + +// Data freshness display labels +export const FRESHNESS_LABELS: Record = { + FRESH: 'Aktuell', + STALE: 'Veraltet', + OUTDATED: 'Abgelaufen', +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..95fc43d --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,99 @@ +import { + DQ_HIGH, DQ_MEDIUM, + CONF_HIGH, CONF_MEDIUM, + SCORE_STRONG, SCORE_MODERATE, + PROB_HIGH, PROB_MEDIUM, +} from './constants' + +// ── Formatting ──────────────────────────────────────────────────────────────── + +export function formatCHF(amount: number, decimals = 0): string { + return new Intl.NumberFormat('de-CH', { + style: 'currency', + currency: 'CHF', + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }).format(amount) +} + +export function formatArea(sqm: number): string { + return `${new Intl.NumberFormat('de-CH').format(sqm)} m²` +} + +export function formatPercent(value: number, decimals = 0): string { + return `${(value * 100).toFixed(decimals)} %` +} + +export function formatDate(iso: string): string { + return new Intl.DateTimeFormat('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso)) +} + +export function formatRelativeDate(iso: string): string { + const diff = Date.now() - new Date(iso).getTime() + const hours = Math.floor(diff / 3_600_000) + if (hours < 1) return 'Gerade eben' + if (hours < 24) return `vor ${hours} Stunde${hours === 1 ? '' : 'n'}` + const days = Math.floor(hours / 24) + if (days < 7) return `vor ${days} Tag${days === 1 ? '' : 'en'}` + return formatDate(iso) +} + +// ── Color helpers (return MUI color token strings) ──────────────────────────── + +export function dataQualityColor(score: number): 'success' | 'warning' | 'error' { + if (score >= DQ_HIGH) return 'success' + if (score >= DQ_MEDIUM) return 'warning' + return 'error' +} + +export function confidenceColor(score: number): 'success' | 'primary' | 'warning' { + if (score >= CONF_HIGH) return 'success' + if (score >= CONF_MEDIUM) return 'primary' + return 'warning' +} + +export function matchScoreColor(score: number): 'success' | 'warning' | 'error' { + if (score >= SCORE_STRONG) return 'success' + if (score >= SCORE_MODERATE) return 'warning' + return 'error' +} + +export function probabilityColor(prob: number): 'success' | 'warning' | 'error' { + if (prob >= PROB_HIGH) return 'success' + if (prob >= PROB_MEDIUM) return 'warning' + return 'error' +} + +// Hex color variants for use in sx (when MUI color tokens aren't enough) +export function dataQualityHex(score: number): string { + if (score >= DQ_HIGH) return '#1a7a4a' + if (score >= DQ_MEDIUM) return '#d97706' + return '#c0392b' +} + +export function confidenceHex(score: number): string { + if (score >= CONF_HIGH) return '#1a7a4a' + if (score >= CONF_MEDIUM) return '#1e3a5f' + return '#d97706' +} + +export function probabilityHex(prob: number): string { + if (prob >= PROB_HIGH) return '#1a7a4a' + if (prob >= PROB_MEDIUM) return '#d97706' + return '#c0392b' +} + +// ── Misc ────────────────────────────────────────────────────────────────────── + +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max) +} + +export function average(values: number[]): number { + if (values.length === 0) return 0 + return values.reduce((a, b) => a + b, 0) / values.length +} + +export function truncate(str: string, maxLen: number): string { + return str.length <= maxLen ? str : str.slice(0, maxLen - 1) + '…' +} diff --git a/src/main.tsx b/src/main.tsx index 48a6589..324e47e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,13 +4,15 @@ import { BrowserRouter } from 'react-router' import { StyledEngineProvider, ThemeProvider, CssBaseline } from '@mui/material' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { theme } from './lib/theme' +import { AuthProvider } from './provider/AuthProvider' +import { STALE_PROPERTIES } from './lib/constants' import './index.css' import App from './App.tsx' const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 5 * 60 * 1000, + staleTime: STALE_PROPERTIES, retry: 1, }, }, @@ -23,7 +25,9 @@ createRoot(document.getElementById('root')!).render( - + + + diff --git a/src/provider/AuthProvider.tsx b/src/provider/AuthProvider.tsx new file mode 100644 index 0000000..c69d396 --- /dev/null +++ b/src/provider/AuthProvider.tsx @@ -0,0 +1,36 @@ +import { createContext, useContext, type ReactNode } from 'react' +import { useSessionStore } from '../stores/sessionStore' +import type { MockUser } from '../stores/sessionStore' + +// Placeholder AuthContext — swap for real auth (Supabase, Auth0, etc.) later. +// All call sites use this context; no component imports sessionStore directly. + +interface AuthContextValue { + user: MockUser | null + isAuthenticated: boolean + isLoading: boolean + login: (user: MockUser) => void + logout: () => void +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: ReactNode }) { + const { currentUser, isAuthenticated, login, logout } = useSessionStore() + + const value: AuthContextValue = { + user: currentUser, + isAuthenticated, + isLoading: false, // always resolved in mock mode + login, + logout, + } + + return {children} +} + +export function useAuth(): AuthContextValue { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used within AuthProvider') + return ctx +}