mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:31:13 -05:00
Fix admin dashboard auto-refresh
This commit is contained in:
@@ -24,6 +24,7 @@ import { Progress } from '@/components/ui/progress';
|
|||||||
import { QueueStatsDashboard } from './QueueStatsDashboard';
|
import { QueueStatsDashboard } from './QueueStatsDashboard';
|
||||||
import { EscalationDialog } from './EscalationDialog';
|
import { EscalationDialog } from './EscalationDialog';
|
||||||
import { ReassignDialog } from './ReassignDialog';
|
import { ReassignDialog } from './ReassignDialog';
|
||||||
|
import { smartMergeArray } from '@/lib/smartStateUpdate';
|
||||||
|
|
||||||
interface ModerationItem {
|
interface ModerationItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -75,15 +76,24 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
const [escalationDialogOpen, setEscalationDialogOpen] = useState(false);
|
const [escalationDialogOpen, setEscalationDialogOpen] = useState(false);
|
||||||
const [reassignDialogOpen, setReassignDialogOpen] = useState(false);
|
const [reassignDialogOpen, setReassignDialogOpen] = useState(false);
|
||||||
const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null);
|
const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null);
|
||||||
|
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
|
||||||
|
const [newItemsCount, setNewItemsCount] = useState(0);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { isAdmin, isSuperuser } = useUserRole();
|
const { isAdmin, isSuperuser } = useUserRole();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const queue = useModerationQueue();
|
const queue = useModerationQueue();
|
||||||
|
|
||||||
// Get admin settings for polling configuration
|
// Get admin settings for polling configuration
|
||||||
const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings();
|
const {
|
||||||
|
getAdminPanelRefreshMode,
|
||||||
|
getAdminPanelPollInterval,
|
||||||
|
getAutoRefreshStrategy,
|
||||||
|
getPreserveInteractionState
|
||||||
|
} = useAdminSettings();
|
||||||
const refreshMode = getAdminPanelRefreshMode();
|
const refreshMode = getAdminPanelRefreshMode();
|
||||||
const pollInterval = getAdminPanelPollInterval();
|
const pollInterval = getAdminPanelPollInterval();
|
||||||
|
const refreshStrategy = getAutoRefreshStrategy();
|
||||||
|
const preserveInteraction = getPreserveInteractionState();
|
||||||
|
|
||||||
// Expose refresh method via ref
|
// Expose refresh method via ref
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -346,7 +356,39 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
// Sort by creation date (newest first for better UX)
|
// Sort by creation date (newest first for better UX)
|
||||||
formattedItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
formattedItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||||
|
|
||||||
setItems(formattedItems);
|
// Use smart merging for silent refreshes if strategy is 'merge'
|
||||||
|
if (silent && refreshStrategy === 'merge') {
|
||||||
|
const mergeResult = smartMergeArray(items, formattedItems, {
|
||||||
|
compareFields: ['status', 'reviewed_at', 'reviewer_notes'],
|
||||||
|
preserveOrder: true,
|
||||||
|
addToTop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there are changes and we should preserve interaction
|
||||||
|
if (mergeResult.hasChanges) {
|
||||||
|
// Filter out items user is interacting with
|
||||||
|
const protectedIds = preserveInteraction ? interactingWith : new Set<string>();
|
||||||
|
const mergedWithProtection = mergeResult.items.map(item => {
|
||||||
|
if (protectedIds.has(item.id)) {
|
||||||
|
// Find and preserve the current version of this item
|
||||||
|
const currentItem = items.find(i => i.id === item.id);
|
||||||
|
return currentItem || item;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
setItems(mergedWithProtection);
|
||||||
|
|
||||||
|
// Update new items count
|
||||||
|
if (mergeResult.changes.added.length > 0) {
|
||||||
|
setNewItemsCount(prev => prev + mergeResult.changes.added.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full replacement for non-silent refreshes or 'replace' strategy
|
||||||
|
setItems(formattedItems);
|
||||||
|
setNewItemsCount(0);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching moderation items:', error);
|
console.error('Error fetching moderation items:', error);
|
||||||
console.error('Error details:', {
|
console.error('Error details:', {
|
||||||
@@ -1985,6 +2027,24 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* New Items Notification */}
|
||||||
|
{newItemsCount > 0 && (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setNewItemsCount(0);
|
||||||
|
fetchItems(activeEntityFilter, activeStatusFilter, false);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 border-primary/50 bg-primary/5 hover:bg-primary/10"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Show {newItemsCount} new {newItemsCount === 1 ? 'item' : 'items'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Queue Content */}
|
{/* Queue Content */}
|
||||||
<QueueContent />
|
<QueueContent />
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
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';
|
||||||
|
import { smartMergeArray } from '@/lib/smartStateUpdate';
|
||||||
|
import { useAdminSettings } from '@/hooks/useAdminSettings';
|
||||||
|
|
||||||
interface ActivityItem {
|
interface ActivityItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,18 +30,25 @@ export interface RecentActivityRef {
|
|||||||
export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
|
export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
|
||||||
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isSilentRefresh, setIsSilentRefresh] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { getAutoRefreshStrategy } = useAdminSettings();
|
||||||
|
const refreshStrategy = getAutoRefreshStrategy();
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
refresh: fetchRecentActivity
|
refresh: () => fetchRecentActivity(false)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const fetchRecentActivity = async () => {
|
const fetchRecentActivity = async (silent = false) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
if (!silent) {
|
||||||
|
setLoading(true);
|
||||||
|
} else {
|
||||||
|
setIsSilentRefresh(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch recent approved/rejected submissions
|
// Fetch recent approved/rejected submissions
|
||||||
const { data: submissions, error: submissionsError } = await supabase
|
const { data: submissions, error: submissionsError } = await supabase
|
||||||
@@ -124,7 +133,23 @@ export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
|
|||||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
setActivities(allActivities.slice(0, 20)); // Keep top 20 most recent
|
const recentActivities = allActivities.slice(0, 20); // Keep top 20 most recent
|
||||||
|
|
||||||
|
// Use smart merging for silent refreshes if strategy is 'merge'
|
||||||
|
if (silent && refreshStrategy === 'merge') {
|
||||||
|
const mergeResult = smartMergeArray(activities, recentActivities, {
|
||||||
|
compareFields: ['timestamp', 'action'],
|
||||||
|
preserveOrder: false,
|
||||||
|
addToTop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mergeResult.hasChanges) {
|
||||||
|
setActivities(mergeResult.items);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full replacement for non-silent refreshes or 'replace' strategy
|
||||||
|
setActivities(recentActivities);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching recent activity:', error);
|
console.error('Error fetching recent activity:', error);
|
||||||
toast({
|
toast({
|
||||||
@@ -133,12 +158,15 @@ export const RecentActivity = forwardRef<RecentActivityRef>((props, ref) => {
|
|||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!silent) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
setIsSilentRefresh(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRecentActivity();
|
fetchRecentActivity(false);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 { smartMergeArray } from '@/lib/smartStateUpdate';
|
||||||
|
|
||||||
interface Report {
|
interface Report {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -49,13 +50,19 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
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 { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Get admin settings for polling configuration
|
// Get admin settings for polling configuration
|
||||||
const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings();
|
const {
|
||||||
|
getAdminPanelRefreshMode,
|
||||||
|
getAdminPanelPollInterval,
|
||||||
|
getAutoRefreshStrategy
|
||||||
|
} = useAdminSettings();
|
||||||
const refreshMode = getAdminPanelRefreshMode();
|
const refreshMode = getAdminPanelRefreshMode();
|
||||||
const pollInterval = getAdminPanelPollInterval();
|
const pollInterval = getAdminPanelPollInterval();
|
||||||
|
const refreshStrategy = getAutoRefreshStrategy();
|
||||||
|
|
||||||
// Expose refresh method via ref
|
// Expose refresh method via ref
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -120,7 +127,27 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
setReports(reportsWithContent);
|
// Use smart merging for silent refreshes if strategy is 'merge'
|
||||||
|
if (silent && refreshStrategy === 'merge') {
|
||||||
|
const mergeResult = smartMergeArray(reports, reportsWithContent, {
|
||||||
|
compareFields: ['status'],
|
||||||
|
preserveOrder: true,
|
||||||
|
addToTop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mergeResult.hasChanges) {
|
||||||
|
setReports(mergeResult.items);
|
||||||
|
|
||||||
|
// Update new reports count
|
||||||
|
if (mergeResult.changes.added.length > 0) {
|
||||||
|
setNewReportsCount(prev => prev + mergeResult.changes.added.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full replacement for non-silent refreshes or 'replace' strategy
|
||||||
|
setReports(reportsWithContent);
|
||||||
|
setNewReportsCount(0);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching reports:', error);
|
console.error('Error fetching reports:', error);
|
||||||
toast({
|
toast({
|
||||||
@@ -213,6 +240,24 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* New Reports Notification */}
|
||||||
|
{newReportsCount > 0 && (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setNewReportsCount(0);
|
||||||
|
fetchReports(false);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 border-destructive/50 bg-destructive/5 hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
<Flag className="w-4 h-4" />
|
||||||
|
Show {newReportsCount} new {newReportsCount === 1 ? 'report' : 'reports'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{reports.map((report) => (
|
{reports.map((report) => (
|
||||||
<Card key={report.id} className="border-l-4 border-l-red-500">
|
<Card key={report.id} className="border-l-4 border-l-red-500">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
|
|||||||
@@ -126,6 +126,30 @@ export function useAdminSettings() {
|
|||||||
return parseInt(value?.toString() || '30') * 1000; // Convert to milliseconds
|
return parseInt(value?.toString() || '30') * 1000; // Convert to milliseconds
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auto-refresh strategy setting
|
||||||
|
* Returns: 'merge' | 'replace' | 'notify'
|
||||||
|
*/
|
||||||
|
const getAutoRefreshStrategy = (): 'merge' | 'replace' | 'notify' => {
|
||||||
|
const value = getSettingValue('auto_refresh_strategy', 'merge');
|
||||||
|
const cleanValue = typeof value === 'string' ? value.replace(/"/g, '') : value;
|
||||||
|
return cleanValue as 'merge' | 'replace' | 'notify';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get preserve interaction state setting
|
||||||
|
* Returns: boolean
|
||||||
|
*/
|
||||||
|
const getPreserveInteractionState = (): boolean => {
|
||||||
|
const value = getSettingValue('preserve_interaction_state', 'true');
|
||||||
|
const cleanValue = typeof value === 'string' ? value.replace(/"/g, '') : value;
|
||||||
|
return cleanValue === 'true' || cleanValue === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotificationRecipients = () => {
|
||||||
|
return getSettingValue('notifications.recipients', []);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -140,10 +164,13 @@ export function useAdminSettings() {
|
|||||||
getRequireApproval,
|
getRequireApproval,
|
||||||
getBanDurations,
|
getBanDurations,
|
||||||
getEmailAlertsEnabled,
|
getEmailAlertsEnabled,
|
||||||
|
getNotificationRecipients,
|
||||||
getReportThreshold,
|
getReportThreshold,
|
||||||
getAuditRetentionDays,
|
getAuditRetentionDays,
|
||||||
getAutoCleanupEnabled,
|
getAutoCleanupEnabled,
|
||||||
getAdminPanelRefreshMode,
|
getAdminPanelRefreshMode,
|
||||||
getAdminPanelPollInterval,
|
getAdminPanelPollInterval,
|
||||||
|
getAutoRefreshStrategy,
|
||||||
|
getPreserveInteractionState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add admin settings for non-disruptive auto-refresh
|
||||||
|
INSERT INTO admin_settings (setting_key, setting_value, category, description)
|
||||||
|
VALUES
|
||||||
|
('auto_refresh_strategy', '"merge"', 'admin_panel', 'Auto-refresh strategy: merge (silently merge new items), replace (current behavior - full replacement), notify (show notification only)'),
|
||||||
|
('preserve_interaction_state', '"true"', 'admin_panel', 'Whether to preserve user interaction state during auto-refresh (expanded items, typing in notes, etc.)')
|
||||||
|
ON CONFLICT (setting_key) DO NOTHING;
|
||||||
Reference in New Issue
Block a user