mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 16:51:13 -05:00
feat: Implement API and cache improvements
This commit is contained in:
@@ -171,3 +171,49 @@ When migrating a component:
|
||||
- [ ] Replace loading state with `mutation.isPending`
|
||||
- [ ] Remove try/catch blocks from component
|
||||
- [ ] Test optimistic updates if applicable
|
||||
- [ ] Add audit log creation where appropriate
|
||||
- [ ] Ensure proper type safety with TypeScript
|
||||
|
||||
## Available Mutation Hooks
|
||||
|
||||
### Profile & User Management
|
||||
- `useProfileUpdateMutation` - Profile updates (username, display name, bio)
|
||||
- `useProfileLocationMutation` - Location and personal info updates
|
||||
- `usePrivacyMutations` - Privacy settings updates
|
||||
|
||||
### Security
|
||||
- `useSecurityMutations` - Session management (revoke sessions)
|
||||
|
||||
### Moderation
|
||||
- `useReportMutation` - Submit user reports
|
||||
- `useReportActionMutation` - Resolve/dismiss reports
|
||||
|
||||
### Admin
|
||||
- `useAuditLogs` - Query audit logs with pagination
|
||||
|
||||
## Cache Invalidation Guidelines
|
||||
|
||||
Always invalidate related caches after mutations:
|
||||
|
||||
```typescript
|
||||
// After profile update
|
||||
invalidateUserProfile(userId);
|
||||
invalidateProfileStats(userId);
|
||||
invalidateProfileActivity(userId);
|
||||
invalidateUserSearch(); // If username/display name changed
|
||||
|
||||
// After privacy update
|
||||
invalidateUserProfile(userId);
|
||||
invalidateAuditLogs(userId);
|
||||
invalidateUserSearch(); // If privacy level changed
|
||||
|
||||
// After report action
|
||||
invalidateModerationQueue();
|
||||
invalidateModerationStats();
|
||||
invalidateAuditLogs();
|
||||
|
||||
// After security action
|
||||
invalidateSessions();
|
||||
invalidateAuditLogs();
|
||||
invalidateEmailChangeStatus(); // For email changes
|
||||
```
|
||||
|
||||
55
src/hooks/admin/useAuditLogs.ts
Normal file
55
src/hooks/admin/useAuditLogs.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
interface AuditLogFilters {
|
||||
userId?: string;
|
||||
action?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for querying audit logs with proper caching
|
||||
* Provides: paginated audit log queries with filtering
|
||||
*/
|
||||
export function useAuditLogs(filters: AuditLogFilters = {}) {
|
||||
const { userId, action, page = 1, pageSize = 50 } = filters;
|
||||
|
||||
return useQuery({
|
||||
queryKey: queryKeys.admin.auditLogs(userId),
|
||||
queryFn: async () => {
|
||||
let query = supabase
|
||||
.from('profile_audit_log')
|
||||
.select('*', { count: 'exact' })
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (userId) {
|
||||
query = query.eq('user_id', userId);
|
||||
}
|
||||
|
||||
if (action) {
|
||||
query = query.eq('action', action);
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize - 1;
|
||||
query = query.range(startIndex, endIndex);
|
||||
|
||||
const { data, error, count } = await query;
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return {
|
||||
logs: data || [],
|
||||
total: count || 0,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil((count || 0) / pageSize),
|
||||
};
|
||||
},
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
68
src/hooks/privacy/useBlockUserMutation.ts
Normal file
68
src/hooks/privacy/useBlockUserMutation.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
interface UnblockUserParams {
|
||||
blockId: string;
|
||||
blockedUserId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for user blocking/unblocking mutations
|
||||
* Provides: unblock user with automatic audit logging and cache invalidation
|
||||
*/
|
||||
export function useBlockUserMutation() {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { invalidateAuditLogs } = useQueryInvalidation();
|
||||
|
||||
const unblockUser = useMutation({
|
||||
mutationFn: async ({ blockId, blockedUserId, username }: UnblockUserParams) => {
|
||||
if (!user) throw new Error('Authentication required');
|
||||
|
||||
const { error } = await supabase
|
||||
.from('user_blocks')
|
||||
.delete()
|
||||
.eq('id', blockId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Log to audit trail
|
||||
await supabase.from('profile_audit_log').insert([{
|
||||
user_id: user.id,
|
||||
changed_by: user.id,
|
||||
action: 'user_unblocked',
|
||||
changes: JSON.parse(JSON.stringify({
|
||||
blocked_user_id: blockedUserId,
|
||||
username,
|
||||
timestamp: new Date().toISOString()
|
||||
}))
|
||||
}]);
|
||||
|
||||
return { blockedUserId, username };
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Error", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, { username }) => {
|
||||
// Invalidate blocked users cache
|
||||
queryClient.invalidateQueries({ queryKey: ['blocked-users'] });
|
||||
invalidateAuditLogs();
|
||||
|
||||
toast.success("User Unblocked", {
|
||||
description: `You have unblocked @${username}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
unblockUser,
|
||||
isUnblocking: unblockUser.isPending,
|
||||
};
|
||||
}
|
||||
52
src/hooks/privacy/useBlockedUsers.ts
Normal file
52
src/hooks/privacy/useBlockedUsers.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import type { UserBlock } from '@/types/privacy';
|
||||
|
||||
/**
|
||||
* Hook for querying blocked users
|
||||
* Provides: list of blocked users with profile information
|
||||
*/
|
||||
export function useBlockedUsers() {
|
||||
const { user } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['blocked-users', user?.id],
|
||||
queryFn: async () => {
|
||||
if (!user) return [];
|
||||
|
||||
// First get the blocked user IDs
|
||||
const { data: blocks, error: blocksError } = await supabase
|
||||
.from('user_blocks')
|
||||
.select('id, blocked_id, reason, created_at')
|
||||
.eq('blocker_id', user.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (blocksError) throw blocksError;
|
||||
|
||||
if (!blocks || blocks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Then get the profile information for blocked users
|
||||
const blockedIds = blocks.map(b => b.blocked_id);
|
||||
const { data: profiles, error: profilesError } = await supabase
|
||||
.from('profiles')
|
||||
.select('user_id, username, display_name, avatar_url')
|
||||
.in('user_id', blockedIds);
|
||||
|
||||
if (profilesError) throw profilesError;
|
||||
|
||||
// Combine the data
|
||||
const blockedUsersWithProfiles: UserBlock[] = blocks.map(block => ({
|
||||
...block,
|
||||
blocker_id: user.id,
|
||||
blocked_profile: profiles?.find(p => p.user_id === block.blocked_id)
|
||||
}));
|
||||
|
||||
return blockedUsersWithProfiles;
|
||||
},
|
||||
enabled: !!user,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
92
src/hooks/privacy/usePrivacyMutations.ts
Normal file
92
src/hooks/privacy/usePrivacyMutations.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import type { PrivacyFormData } from '@/types/privacy';
|
||||
|
||||
/**
|
||||
* Hook for privacy settings mutations
|
||||
* Provides: privacy settings updates with automatic audit logging and cache invalidation
|
||||
*/
|
||||
export function usePrivacyMutations() {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
invalidateUserProfile,
|
||||
invalidateAuditLogs,
|
||||
invalidateUserSearch
|
||||
} = useQueryInvalidation();
|
||||
|
||||
const updatePrivacy = useMutation({
|
||||
mutationFn: async (data: PrivacyFormData) => {
|
||||
if (!user) throw new Error('Authentication required');
|
||||
|
||||
// Update profile privacy settings
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
privacy_level: data.privacy_level,
|
||||
show_pronouns: data.show_pronouns,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
// Extract privacy settings (exclude profile fields)
|
||||
const { privacy_level, show_pronouns, ...privacySettings } = data;
|
||||
|
||||
// Update user preferences
|
||||
const { error: prefsError } = await supabase
|
||||
.from('user_preferences')
|
||||
.upsert([{
|
||||
user_id: user.id,
|
||||
privacy_settings: privacySettings,
|
||||
updated_at: new Date().toISOString()
|
||||
}]);
|
||||
|
||||
if (prefsError) throw prefsError;
|
||||
|
||||
// Log to audit trail
|
||||
await supabase.from('profile_audit_log').insert([{
|
||||
user_id: user.id,
|
||||
changed_by: user.id,
|
||||
action: 'privacy_settings_updated',
|
||||
changes: {
|
||||
updated: privacySettings,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}]);
|
||||
|
||||
return { privacySettings };
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Update Failed", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
// Invalidate all related caches
|
||||
if (user) {
|
||||
invalidateUserProfile(user.id);
|
||||
invalidateAuditLogs(user.id);
|
||||
|
||||
// If privacy level changed, invalidate user search
|
||||
if (variables.privacy_level) {
|
||||
invalidateUserSearch();
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Privacy Updated", {
|
||||
description: "Your privacy preferences have been successfully saved.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updatePrivacy,
|
||||
isUpdating: updatePrivacy.isPending,
|
||||
};
|
||||
}
|
||||
82
src/hooks/profile/useProfileLocationMutation.ts
Normal file
82
src/hooks/profile/useProfileLocationMutation.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import type { LocationFormData } from '@/types/location';
|
||||
|
||||
/**
|
||||
* Hook for profile location mutations
|
||||
* Provides: location updates with automatic audit logging and cache invalidation
|
||||
*/
|
||||
export function useProfileLocationMutation() {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
invalidateUserProfile,
|
||||
invalidateAuditLogs
|
||||
} = useQueryInvalidation();
|
||||
|
||||
const updateLocation = useMutation({
|
||||
mutationFn: async (data: LocationFormData) => {
|
||||
if (!user) throw new Error('Authentication required');
|
||||
|
||||
const previousProfile = {
|
||||
personal_location: data.personal_location,
|
||||
home_park_id: data.home_park_id,
|
||||
timezone: data.timezone,
|
||||
preferred_language: data.preferred_language,
|
||||
preferred_pronouns: data.preferred_pronouns
|
||||
};
|
||||
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
preferred_pronouns: data.preferred_pronouns || null,
|
||||
timezone: data.timezone,
|
||||
preferred_language: data.preferred_language,
|
||||
personal_location: data.personal_location || null,
|
||||
home_park_id: data.home_park_id || null,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
// Log to audit trail
|
||||
await supabase.from('profile_audit_log').insert([{
|
||||
user_id: user.id,
|
||||
changed_by: user.id,
|
||||
action: 'location_info_updated',
|
||||
changes: JSON.parse(JSON.stringify({
|
||||
previous: { profile: previousProfile },
|
||||
updated: { profile: data },
|
||||
timestamp: new Date().toISOString()
|
||||
}))
|
||||
}]);
|
||||
|
||||
return data;
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Update Failed", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (user) {
|
||||
invalidateUserProfile(user.id);
|
||||
invalidateAuditLogs(user.id);
|
||||
}
|
||||
|
||||
toast.success("Settings Saved", {
|
||||
description: "Your location and personal information have been updated.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateLocation,
|
||||
isUpdating: updateLocation.isPending,
|
||||
};
|
||||
}
|
||||
86
src/hooks/reports/useReportActionMutation.ts
Normal file
86
src/hooks/reports/useReportActionMutation.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
interface ReportActionParams {
|
||||
reportId: string;
|
||||
action: 'reviewed' | 'dismissed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for report action mutations
|
||||
* Provides: report resolution/dismissal with automatic audit logging and cache invalidation
|
||||
*/
|
||||
export function useReportActionMutation() {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { invalidateModerationQueue, invalidateModerationStats, invalidateAuditLogs } = useQueryInvalidation();
|
||||
|
||||
const resolveReport = useMutation({
|
||||
mutationFn: async ({ reportId, action }: ReportActionParams) => {
|
||||
if (!user) throw new Error('Authentication required');
|
||||
|
||||
// Fetch full report details for audit log
|
||||
const { data: reportData } = await supabase
|
||||
.from('reports')
|
||||
.select('reporter_id, reported_entity_type, reported_entity_id, reason')
|
||||
.eq('id', reportId)
|
||||
.single();
|
||||
|
||||
const { error } = await supabase
|
||||
.from('reports')
|
||||
.update({
|
||||
status: action,
|
||||
reviewed_by: user.id,
|
||||
reviewed_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', reportId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Log audit trail for report resolution
|
||||
if (reportData) {
|
||||
try {
|
||||
await supabase.rpc('log_admin_action', {
|
||||
_admin_user_id: user.id,
|
||||
_target_user_id: reportData.reporter_id,
|
||||
_action: action === 'reviewed' ? 'report_resolved' : 'report_dismissed',
|
||||
_details: {
|
||||
report_id: reportId,
|
||||
reported_entity_type: reportData.reported_entity_type,
|
||||
reported_entity_id: reportData.reported_entity_id,
|
||||
report_reason: reportData.reason,
|
||||
action: action
|
||||
}
|
||||
});
|
||||
} catch (auditError) {
|
||||
console.error('Failed to log report action audit:', auditError);
|
||||
}
|
||||
}
|
||||
|
||||
return { action, reportData };
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Error", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, { action }) => {
|
||||
invalidateModerationQueue();
|
||||
invalidateModerationStats();
|
||||
invalidateAuditLogs();
|
||||
|
||||
toast.success(`Report ${action}`, {
|
||||
description: `The report has been marked as ${action}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
resolveReport,
|
||||
isResolving: resolveReport.isPending,
|
||||
};
|
||||
}
|
||||
54
src/hooks/security/useSecurityMutations.ts
Normal file
54
src/hooks/security/useSecurityMutations.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
|
||||
interface RevokeSessionParams {
|
||||
sessionId: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for session management mutations
|
||||
* Provides: session revocation with automatic cache invalidation
|
||||
*/
|
||||
export function useSecurityMutations() {
|
||||
const queryClient = useQueryClient();
|
||||
const { invalidateSessions, invalidateAuditLogs } = useQueryInvalidation();
|
||||
|
||||
const revokeSession = useMutation({
|
||||
mutationFn: async ({ sessionId }: RevokeSessionParams) => {
|
||||
const { error } = await supabase.rpc('revoke_my_session', {
|
||||
session_id: sessionId
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Error", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, { isCurrent }) => {
|
||||
invalidateSessions();
|
||||
invalidateAuditLogs();
|
||||
|
||||
toast.success("Success", {
|
||||
description: "Session revoked successfully",
|
||||
});
|
||||
|
||||
// Redirect to login if current session was revoked
|
||||
if (isCurrent) {
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth';
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
revokeSession,
|
||||
isRevoking: revokeSession.isPending,
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from './useAuth';
|
||||
import { useUserRole } from './useUserRole';
|
||||
import { useToast } from './use-toast';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
interface AdminSetting {
|
||||
id: string;
|
||||
@@ -24,7 +25,7 @@ export function useAdminSettings() {
|
||||
isLoading,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['admin-settings'],
|
||||
queryKey: queryKeys.admin.settings(),
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('admin_settings')
|
||||
@@ -59,7 +60,7 @@ export function useAdminSettings() {
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.admin.settings() });
|
||||
toast({
|
||||
title: "Setting Updated",
|
||||
description: "The setting has been saved successfully.",
|
||||
|
||||
@@ -331,6 +331,26 @@ export function useQueryInvalidation() {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate email change status cache
|
||||
* Call this after email change operations
|
||||
*/
|
||||
invalidateEmailChangeStatus: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.security.emailChangeStatus()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate sessions cache
|
||||
* Call this after session operations (login, logout, revoke)
|
||||
*/
|
||||
invalidateSessions: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.security.sessions()
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate security queries
|
||||
* Call this after security-related changes (email, sessions)
|
||||
|
||||
@@ -80,6 +80,7 @@ import { logger } from '@/lib/logger';
|
||||
import { contactCategories } from '@/lib/contactValidation';
|
||||
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
interface ContactSubmission {
|
||||
id: string;
|
||||
@@ -159,7 +160,7 @@ export default function AdminContact() {
|
||||
|
||||
// Fetch contact submissions
|
||||
const { data: submissions, isLoading } = useQuery({
|
||||
queryKey: ['admin-contact-submissions', statusFilter, categoryFilter, searchQuery, showArchived],
|
||||
queryKey: queryKeys.admin.contactSubmissions(statusFilter, categoryFilter, searchQuery, showArchived),
|
||||
queryFn: async () => {
|
||||
let query = supabase
|
||||
.from('contact_submissions')
|
||||
@@ -282,7 +283,10 @@ export default function AdminContact() {
|
||||
.order('created_at', { ascending: true })
|
||||
.then(({ data }) => setEmailThreads((data as EmailThread[]) || []));
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-contact-submissions'],
|
||||
exact: false
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
handleError(error, { action: 'Send Email Reply' });
|
||||
@@ -320,7 +324,10 @@ export default function AdminContact() {
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-contact-submissions'],
|
||||
exact: false
|
||||
});
|
||||
handleSuccess('Status Updated', 'Contact submission status has been updated');
|
||||
setSelectedSubmission(null);
|
||||
setAdminNotes('');
|
||||
@@ -345,7 +352,10 @@ export default function AdminContact() {
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-contact-submissions'],
|
||||
exact: false
|
||||
});
|
||||
handleSuccess('Archived', 'Contact submission has been archived');
|
||||
setSelectedSubmission(null);
|
||||
},
|
||||
@@ -368,7 +378,10 @@ export default function AdminContact() {
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-contact-submissions'],
|
||||
exact: false
|
||||
});
|
||||
handleSuccess('Restored', 'Contact submission has been restored from archive');
|
||||
setSelectedSubmission(null);
|
||||
},
|
||||
@@ -388,7 +401,10 @@ export default function AdminContact() {
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-contact-submissions'],
|
||||
exact: false
|
||||
});
|
||||
handleSuccess('Deleted', 'Contact submission has been permanently deleted');
|
||||
setSelectedSubmission(null);
|
||||
},
|
||||
@@ -428,7 +444,10 @@ export default function AdminContact() {
|
||||
};
|
||||
|
||||
const handleRefreshSubmissions = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-contact-submissions'],
|
||||
exact: false
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyTicket = (ticketNumber: string) => {
|
||||
|
||||
Reference in New Issue
Block a user