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