Skip to content

Commit 2adbca8

Browse files
Implement exception inbox system
- Add complete three-registries backend: expectations and exceptions tables, enums, clues for immutable beliefs and judgments - Create frontend scaffold for Exception Inbox UI with workflow and actions - Wire up hooks for expectations and exceptions, plus initial exception dashboard components and routes X-Lovable-Edit-ID: edt-2297655a-4f83-482a-8f9c-4817ba7f756c
2 parents f47a842 + 4701f48 commit 2adbca8

File tree

9 files changed

+1932
-0
lines changed

9 files changed

+1932
-0
lines changed

src/hooks/useExceptions.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2+
import { supabase } from '@/integrations/supabase/client'
3+
import { useAuth } from '@/contexts/AuthContext'
4+
import { toast } from 'sonner'
5+
import type { ExceptionStatus, ExceptionWithExpectation } from '@/integrations/supabase/types/tables/expectations'
6+
7+
interface ExceptionStats {
8+
open_count: number
9+
acknowledged_count: number
10+
resolved_count: number
11+
dismissed_count: number
12+
total_count: number
13+
avg_resolution_time_hours: number | null
14+
}
15+
16+
interface UseExceptionsOptions {
17+
status?: ExceptionStatus | 'all'
18+
limit?: number
19+
}
20+
21+
export function useExceptions(options: UseExceptionsOptions = {}) {
22+
const { status = 'all', limit = 50 } = options
23+
const { profile } = useAuth()
24+
const queryClient = useQueryClient()
25+
26+
// Fetch exceptions with their expectations
27+
const {
28+
data: exceptions = [],
29+
isLoading,
30+
error,
31+
refetch,
32+
} = useQuery({
33+
queryKey: ['exceptions', profile?.tenant_id, status, limit],
34+
queryFn: async () => {
35+
if (!profile?.tenant_id) return []
36+
37+
let query = supabase
38+
.from('exceptions')
39+
.select(`
40+
*,
41+
expectation:expectations(
42+
id,
43+
entity_type,
44+
entity_id,
45+
expectation_type,
46+
belief_statement,
47+
expected_value,
48+
expected_at,
49+
version,
50+
source,
51+
context
52+
),
53+
acknowledger:profiles!exceptions_acknowledged_by_fkey(id, full_name),
54+
resolver:profiles!exceptions_resolved_by_fkey(id, full_name)
55+
`)
56+
.eq('tenant_id', profile.tenant_id)
57+
.order('detected_at', { ascending: false })
58+
.limit(limit)
59+
60+
if (status !== 'all') {
61+
query = query.eq('status', status)
62+
}
63+
64+
const { data, error } = await query
65+
66+
if (error) throw error
67+
return (data || []) as unknown as ExceptionWithExpectation[]
68+
},
69+
enabled: !!profile?.tenant_id,
70+
})
71+
72+
// Fetch exception stats
73+
const { data: stats } = useQuery({
74+
queryKey: ['exception-stats', profile?.tenant_id],
75+
queryFn: async () => {
76+
if (!profile?.tenant_id) return null
77+
78+
const { data, error } = await supabase.rpc('get_exception_stats', {
79+
p_tenant_id: profile.tenant_id,
80+
})
81+
82+
if (error) throw error
83+
return data?.[0] as ExceptionStats | null
84+
},
85+
enabled: !!profile?.tenant_id,
86+
})
87+
88+
// Acknowledge exception
89+
const acknowledgeMutation = useMutation({
90+
mutationFn: async (exceptionId: string) => {
91+
const { error } = await supabase.rpc('acknowledge_exception', {
92+
p_exception_id: exceptionId,
93+
})
94+
if (error) throw error
95+
},
96+
onSuccess: () => {
97+
queryClient.invalidateQueries({ queryKey: ['exceptions'] })
98+
queryClient.invalidateQueries({ queryKey: ['exception-stats'] })
99+
toast.success('Exception acknowledged')
100+
},
101+
onError: (error) => {
102+
toast.error('Failed to acknowledge exception')
103+
console.error(error)
104+
},
105+
})
106+
107+
// Resolve exception
108+
const resolveMutation = useMutation({
109+
mutationFn: async ({
110+
exceptionId,
111+
rootCause,
112+
correctiveAction,
113+
preventiveAction,
114+
resolution,
115+
}: {
116+
exceptionId: string
117+
rootCause?: string
118+
correctiveAction?: string
119+
preventiveAction?: string
120+
resolution?: Record<string, unknown>
121+
}) => {
122+
const { error } = await supabase.rpc('resolve_exception', {
123+
p_exception_id: exceptionId,
124+
p_root_cause: rootCause || null,
125+
p_corrective_action: correctiveAction || null,
126+
p_preventive_action: preventiveAction || null,
127+
p_resolution: (resolution as any) || null,
128+
})
129+
if (error) throw error
130+
},
131+
onSuccess: () => {
132+
queryClient.invalidateQueries({ queryKey: ['exceptions'] })
133+
queryClient.invalidateQueries({ queryKey: ['exception-stats'] })
134+
toast.success('Exception resolved')
135+
},
136+
onError: (error) => {
137+
toast.error('Failed to resolve exception')
138+
console.error(error)
139+
},
140+
})
141+
142+
// Dismiss exception
143+
const dismissMutation = useMutation({
144+
mutationFn: async ({
145+
exceptionId,
146+
reason,
147+
}: {
148+
exceptionId: string
149+
reason?: string
150+
}) => {
151+
const { error } = await supabase.rpc('dismiss_exception', {
152+
p_exception_id: exceptionId,
153+
p_reason: reason || null,
154+
})
155+
if (error) throw error
156+
},
157+
onSuccess: () => {
158+
queryClient.invalidateQueries({ queryKey: ['exceptions'] })
159+
queryClient.invalidateQueries({ queryKey: ['exception-stats'] })
160+
toast.success('Exception dismissed')
161+
},
162+
onError: (error) => {
163+
toast.error('Failed to dismiss exception')
164+
console.error(error)
165+
},
166+
})
167+
168+
return {
169+
exceptions,
170+
stats,
171+
isLoading,
172+
error,
173+
refetch,
174+
acknowledge: acknowledgeMutation.mutate,
175+
resolve: resolveMutation.mutate,
176+
dismiss: dismissMutation.mutate,
177+
isAcknowledging: acknowledgeMutation.isPending,
178+
isResolving: resolveMutation.isPending,
179+
isDismissing: dismissMutation.isPending,
180+
}
181+
}
182+
183+
export function useException(exceptionId: string | undefined) {
184+
const { profile } = useAuth()
185+
186+
return useQuery({
187+
queryKey: ['exception', exceptionId],
188+
queryFn: async () => {
189+
if (!exceptionId || !profile?.tenant_id) return null
190+
191+
const { data, error } = await supabase
192+
.from('exceptions')
193+
.select(`
194+
*,
195+
expectation:expectations(
196+
*
197+
),
198+
acknowledger:profiles!exceptions_acknowledged_by_fkey(id, full_name, email),
199+
resolver:profiles!exceptions_resolved_by_fkey(id, full_name, email)
200+
`)
201+
.eq('id', exceptionId)
202+
.eq('tenant_id', profile.tenant_id)
203+
.single()
204+
205+
if (error) throw error
206+
return data as unknown as ExceptionWithExpectation
207+
},
208+
enabled: !!exceptionId && !!profile?.tenant_id,
209+
})
210+
}

0 commit comments

Comments
 (0)