mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 15:11:12 -05:00
feat: Execute production readiness plan
This commit is contained in:
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { MapPin, Loader2, X } from 'lucide-react';
|
import { MapPin, Loader2, X } from 'lucide-react';
|
||||||
import { ParkLocationMap } from '@/components/maps/ParkLocationMap';
|
import { ParkLocationMap } from '@/components/maps/ParkLocationMap';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
interface LocationResult {
|
interface LocationResult {
|
||||||
place_id: number;
|
place_id: number;
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { type DependencyConflict, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
import { type DependencyConflict, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||||
|
|
||||||
interface ConflictResolutionDialogProps {
|
interface ConflictResolutionDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -26,7 +25,6 @@ export function ConflictResolutionDialog({
|
|||||||
onResolve,
|
onResolve,
|
||||||
}: ConflictResolutionDialogProps) {
|
}: ConflictResolutionDialogProps) {
|
||||||
const [resolutions, setResolutions] = useState<Record<string, string>>({});
|
const [resolutions, setResolutions] = useState<Record<string, string>>({});
|
||||||
const { toast } = useToast();
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const handleResolutionChange = (itemId: string, action: string) => {
|
const handleResolutionChange = (itemId: string, action: string) => {
|
||||||
@@ -39,10 +37,9 @@ export function ConflictResolutionDialog({
|
|||||||
|
|
||||||
const handleApply = async () => {
|
const handleApply = async () => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
toast({
|
handleError(new Error('Authentication required'), {
|
||||||
title: 'Authentication Required',
|
action: 'Resolve Conflicts',
|
||||||
description: 'You must be logged in to resolve conflicts',
|
metadata: { conflictCount: conflicts.length }
|
||||||
variant: 'destructive',
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,27 +50,22 @@ export function ConflictResolutionDialog({
|
|||||||
const result = await resolveConflicts(conflicts, resolutions, items, user.id);
|
const result = await resolveConflicts(conflicts, resolutions, items, user.id);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
toast({
|
handleError(new Error(result.error || 'Failed to resolve conflicts'), {
|
||||||
title: 'Resolution Failed',
|
action: 'Resolve Conflicts',
|
||||||
description: result.error || 'Failed to resolve conflicts',
|
userId: user.id,
|
||||||
variant: 'destructive',
|
metadata: { conflictCount: conflicts.length }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
handleSuccess('Conflicts Resolved', 'All conflicts have been resolved successfully');
|
||||||
title: 'Conflicts Resolved',
|
|
||||||
description: 'All conflicts have been resolved successfully',
|
|
||||||
});
|
|
||||||
|
|
||||||
onResolve();
|
onResolve();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Resolve Conflicts',
|
||||||
title: 'Error',
|
userId: user.id,
|
||||||
description: errorMsg,
|
metadata: { conflictCount: conflicts.length }
|
||||||
variant: 'destructive',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { Image as ImageIcon } from 'lucide-react';
|
import { Image as ImageIcon } from 'lucide-react';
|
||||||
import { PhotoModal } from './PhotoModal';
|
import { PhotoModal } from './PhotoModal';
|
||||||
|
import { handleError } from '@/lib/errorHandler';
|
||||||
|
|
||||||
interface EntityEditPreviewProps {
|
interface EntityEditPreviewProps {
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
@@ -162,8 +163,11 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
|||||||
|
|
||||||
setChangedFields(changed);
|
setChangedFields(changed);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('EntityEditPreview.fetchSubmissionItems: Error fetching submission items:', error);
|
handleError(error, {
|
||||||
|
action: 'Load Submission Preview',
|
||||||
|
metadata: { submissionId, entityType }
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,7 +26,6 @@ interface UserProfile {
|
|||||||
export function ProfileManager() {
|
export function ProfileManager() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { permissions, loading: roleLoading } = useUserRole();
|
const { permissions, loading: roleLoading } = useUserRole();
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const [profiles, setProfiles] = useState<UserProfile[]>([]);
|
const [profiles, setProfiles] = useState<UserProfile[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -69,12 +68,10 @@ export function ProfileManager() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setProfiles(profilesWithRoles);
|
setProfiles(profilesWithRoles);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error fetching profiles:', error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Load User Profiles',
|
||||||
title: "Error",
|
userId: user?.id
|
||||||
description: "Failed to fetch user profiles.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -105,19 +102,15 @@ export function ProfileManager() {
|
|||||||
|
|
||||||
if (logError) console.error('Error logging action:', logError);
|
if (logError) console.error('Error logging action:', logError);
|
||||||
|
|
||||||
toast({
|
handleSuccess('Success', `User ${ban ? 'banned' : 'unbanned'} successfully.`);
|
||||||
title: "Success",
|
|
||||||
description: `User ${ban ? 'banned' : 'unbanned'} successfully.`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh profiles
|
// Refresh profiles
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error updating user ban status:', error);
|
handleError(error, {
|
||||||
toast({
|
action: `${ban ? 'Ban' : 'Unban'} User`,
|
||||||
title: "Error",
|
userId: user?.id,
|
||||||
description: `Failed to ${ban ? 'ban' : 'unban'} user.`,
|
metadata: { targetUserId, ban }
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
@@ -144,19 +137,19 @@ export function ProfileManager() {
|
|||||||
} else {
|
} else {
|
||||||
// Check permissions before allowing role assignment
|
// Check permissions before allowing role assignment
|
||||||
if (newRole === 'admin' && !permissions.can_manage_admin_roles) {
|
if (newRole === 'admin' && !permissions.can_manage_admin_roles) {
|
||||||
toast({
|
handleError(new Error('Insufficient permissions'), {
|
||||||
title: "Access Denied",
|
action: 'Assign Admin Role',
|
||||||
description: "You don't have permission to assign admin roles.",
|
userId: user?.id,
|
||||||
variant: "destructive",
|
metadata: { targetUserId, attemptedRole: newRole }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newRole === 'superuser') {
|
if (newRole === 'superuser') {
|
||||||
toast({
|
handleError(new Error('Cannot assign superuser via UI'), {
|
||||||
title: "Access Denied",
|
action: 'Assign Superuser Role',
|
||||||
description: "Superuser roles can only be assigned directly in the database.",
|
userId: user?.id,
|
||||||
variant: "destructive",
|
metadata: { targetUserId }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,19 +177,15 @@ export function ProfileManager() {
|
|||||||
|
|
||||||
if (logError) console.error('Error logging action:', logError);
|
if (logError) console.error('Error logging action:', logError);
|
||||||
|
|
||||||
toast({
|
handleSuccess('Success', 'User role updated successfully.');
|
||||||
title: "Success",
|
|
||||||
description: `User role updated successfully.`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh profiles
|
// Refresh profiles
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error updating user role:', error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Update User Role',
|
||||||
title: "Error",
|
userId: user?.id,
|
||||||
description: "Failed to update user role.",
|
metadata: { targetUserId, newRole, previousRoles: currentRoles }
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { ValidationSummary } from './ValidationSummary';
|
|||||||
import type { ValidationResult } from '@/lib/entityValidationSchemas';
|
import type { ValidationResult } from '@/lib/entityValidationSchemas';
|
||||||
import type { LockStatus } from '@/lib/moderation/lockHelpers';
|
import type { LockStatus } from '@/lib/moderation/lockHelpers';
|
||||||
import type { ModerationItem } from '@/types/moderation';
|
import type { ModerationItem } from '@/types/moderation';
|
||||||
|
import { handleError } from '@/lib/errorHandler';
|
||||||
|
|
||||||
interface QueueItemProps {
|
interface QueueItemProps {
|
||||||
item: ModerationItem;
|
item: ModerationItem;
|
||||||
@@ -105,8 +106,11 @@ export const QueueItem = memo(({
|
|||||||
try {
|
try {
|
||||||
await onClaimSubmission(item.id);
|
await onClaimSubmission(item.id);
|
||||||
// On success, component will re-render with new lock state
|
// On success, component will re-render with new lock state
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to claim submission:', error);
|
handleError(error, {
|
||||||
|
action: 'Claim Submission',
|
||||||
|
metadata: { submissionId: item.id }
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Always reset claiming state, even on success
|
// Always reset claiming state, even on success
|
||||||
setIsClaiming(false);
|
setIsClaiming(false);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { handleError } from '@/lib/errorHandler';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
interface Moderator {
|
interface Moderator {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
@@ -45,7 +45,6 @@ export function ReassignDialog({
|
|||||||
const [moderators, setModerators] = useState<Moderator[]>([]);
|
const [moderators, setModerators] = useState<Moderator[]>([]);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -88,13 +87,10 @@ export function ReassignDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setModerators(moderatorsList);
|
setModerators(moderatorsList);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
console.error('Error fetching moderators:', errorMsg);
|
action: 'Load Moderators List',
|
||||||
toast({
|
metadata: { submissionType }
|
||||||
title: 'Error',
|
|
||||||
description: 'Failed to load moderators list',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { handleError } from '@/lib/errorHandler';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
|
||||||
import { ActivityCard } from './ActivityCard';
|
import { ActivityCard } from './ActivityCard';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Activity as ActivityIcon } from 'lucide-react';
|
import { Activity as ActivityIcon } from 'lucide-react';
|
||||||
@@ -33,7 +32,6 @@ export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isSilentRefresh, setIsSilentRefresh] = useState(false);
|
const [isSilentRefresh, setIsSilentRefresh] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { toast } = useToast();
|
|
||||||
const { getAutoRefreshStrategy } = useAdminSettings();
|
const { getAutoRefreshStrategy } = useAdminSettings();
|
||||||
const refreshStrategy = getAutoRefreshStrategy();
|
const refreshStrategy = getAutoRefreshStrategy();
|
||||||
|
|
||||||
@@ -151,13 +149,10 @@ export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
|
|||||||
// Full replacement for non-silent refreshes or 'replace' strategy
|
// Full replacement for non-silent refreshes or 'replace' strategy
|
||||||
setActivities(recentActivities);
|
setActivities(recentActivities);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
console.error('Error fetching recent activity:', errorMsg);
|
action: 'Load Recent Activity',
|
||||||
toast({
|
userId: user?.id
|
||||||
title: "Error",
|
|
||||||
description: "Failed to load recent activity",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import {
|
|||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from '@/components/ui/pagination';
|
} from '@/components/ui/pagination';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { smartMergeArray } from '@/lib/smartStateUpdate';
|
import { smartMergeArray } from '@/lib/smartStateUpdate';
|
||||||
|
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||||
|
|
||||||
// Type-safe reported content interfaces
|
// Type-safe reported content interfaces
|
||||||
interface ReportedReview {
|
interface ReportedReview {
|
||||||
@@ -112,7 +112,6 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||||
const [newReportsCount, setNewReportsCount] = useState(0);
|
const [newReportsCount, setNewReportsCount] = useState(0);
|
||||||
const { toast } = useToast();
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
@@ -301,12 +300,11 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
setReports(reportsWithContent);
|
setReports(reportsWithContent);
|
||||||
setNewReportsCount(0);
|
setNewReportsCount(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error fetching reports:', error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Load Reports',
|
||||||
title: "Error",
|
userId: user?.id,
|
||||||
description: "Failed to load reports queue",
|
metadata: { currentPage, pageSize }
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Only clear loading if it was set
|
// Only clear loading if it was set
|
||||||
@@ -352,10 +350,7 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
toast({
|
handleSuccess(`Report ${action}`, `The report has been marked as ${action}`);
|
||||||
title: `Report ${action}`,
|
|
||||||
description: `The report has been marked as ${action}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove report from queue
|
// Remove report from queue
|
||||||
setReports(prev => {
|
setReports(prev => {
|
||||||
@@ -366,12 +361,11 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
}
|
}
|
||||||
return newReports;
|
return newReports;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error updating report:', error);
|
handleError(error, {
|
||||||
toast({
|
action: `${action === 'reviewed' ? 'Resolve' : 'Dismiss'} Report`,
|
||||||
title: "Error",
|
userId: user?.id,
|
||||||
description: `Failed to ${action} report`,
|
metadata: { reportId, action }
|
||||||
variant: "destructive",
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||||
|
|
||||||
// Type-safe role definitions
|
// Type-safe role definitions
|
||||||
const VALID_ROLES = ['admin', 'moderator', 'user'] as const;
|
const VALID_ROLES = ['admin', 'moderator', 'user'] as const;
|
||||||
@@ -66,9 +66,6 @@ export function UserRoleManager() {
|
|||||||
isSuperuser,
|
isSuperuser,
|
||||||
permissions
|
permissions
|
||||||
} = useUserRole();
|
} = useUserRole();
|
||||||
const {
|
|
||||||
toast
|
|
||||||
} = useToast();
|
|
||||||
const fetchUserRoles = async () => {
|
const fetchUserRoles = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -99,12 +96,10 @@ export function UserRoleManager() {
|
|||||||
profiles: profileMap.get(role.user_id)
|
profiles: profileMap.get(role.user_id)
|
||||||
}));
|
}));
|
||||||
setUserRoles(userRolesWithProfiles);
|
setUserRoles(userRolesWithProfiles);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error fetching user roles:', error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Load User Roles',
|
||||||
title: "Error",
|
userId: user?.id
|
||||||
description: "Failed to load user roles",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -144,10 +139,10 @@ export function UserRoleManager() {
|
|||||||
|
|
||||||
// Double-check role validity before database operation
|
// Double-check role validity before database operation
|
||||||
if (!isValidRole(role)) {
|
if (!isValidRole(role)) {
|
||||||
toast({
|
handleError(new Error('Invalid role'), {
|
||||||
title: "Invalid Role",
|
action: 'Grant Role',
|
||||||
description: "The selected role is not valid",
|
userId: user?.id,
|
||||||
variant: "destructive"
|
metadata: { targetUserId: userId, attemptedRole: role }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,20 +159,16 @@ export function UserRoleManager() {
|
|||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
toast({
|
handleSuccess('Role Granted', `User has been granted ${getRoleLabel(role)} role`);
|
||||||
title: "Role Granted",
|
|
||||||
description: `User has been granted ${getRoleLabel(role)} role`
|
|
||||||
});
|
|
||||||
setNewUserSearch('');
|
setNewUserSearch('');
|
||||||
setNewRole('');
|
setNewRole('');
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
fetchUserRoles();
|
fetchUserRoles();
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error granting role:', error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Grant Role',
|
||||||
title: "Error",
|
userId: user?.id,
|
||||||
description: "Failed to grant role",
|
metadata: { targetUserId: userId, role }
|
||||||
variant: "destructive"
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
@@ -191,17 +182,13 @@ export function UserRoleManager() {
|
|||||||
error
|
error
|
||||||
} = await supabase.from('user_roles').delete().eq('id', roleId);
|
} = await supabase.from('user_roles').delete().eq('id', roleId);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
toast({
|
handleSuccess('Role Revoked', 'User role has been revoked');
|
||||||
title: "Role Revoked",
|
|
||||||
description: "User role has been revoked"
|
|
||||||
});
|
|
||||||
fetchUserRoles();
|
fetchUserRoles();
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error revoking role:', error);
|
handleError(error, {
|
||||||
toast({
|
action: 'Revoke Role',
|
||||||
title: "Error",
|
userId: user?.id,
|
||||||
description: "Failed to revoke role",
|
metadata: { roleId }
|
||||||
variant: "destructive"
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
@@ -273,10 +260,10 @@ export function UserRoleManager() {
|
|||||||
} else if (selectedUser && newRole) {
|
} else if (selectedUser && newRole) {
|
||||||
// This should never happen due to Select component constraints,
|
// This should never happen due to Select component constraints,
|
||||||
// but provides safety in case of UI bugs
|
// but provides safety in case of UI bugs
|
||||||
toast({
|
handleError(new Error('Invalid role selected'), {
|
||||||
title: "Invalid Role",
|
action: 'Grant Role',
|
||||||
description: "Please select a valid role",
|
userId: user?.id,
|
||||||
variant: "destructive"
|
metadata: { selectedUser: selectedUser?.user_id, newRole }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}} disabled={!newRole || !isValidRole(newRole) || !searchResults.find(p => (p.display_name || p.username) === newUserSearch) || actionLoading === 'grant'} className="w-full md:w-auto">
|
}} disabled={!newRole || !isValidRole(newRole) || !searchResults.find(p => (p.display_name || p.username) === newUserSearch) || actionLoading === 'grant'} className="w-full md:w-auto">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
import { validateEntityData, ValidationResult } from '@/lib/entityValidationSchemas';
|
import { validateEntityData, ValidationResult } from '@/lib/entityValidationSchemas';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
interface ValidationSummaryProps {
|
interface ValidationSummaryProps {
|
||||||
item: {
|
item: {
|
||||||
@@ -76,8 +77,12 @@ export function ValidationSummary({ item, onValidationChange, compact = false, v
|
|||||||
|
|
||||||
setValidationResult(result);
|
setValidationResult(result);
|
||||||
onValidationChange?.(result);
|
onValidationChange?.(result);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Validation error:', error);
|
logger.error('Entity validation failed', {
|
||||||
|
action: 'validate_entity',
|
||||||
|
entityType: item.item_type,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
setValidationResult({
|
setValidationResult({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
blockingErrors: [{ field: 'validation', message: 'Failed to validate', severity: 'blocking' }],
|
blockingErrors: [{ field: 'validation', message: 'Failed to validate', severity: 'blocking' }],
|
||||||
|
|||||||
Reference in New Issue
Block a user