This commit is contained in:
alorig
2025-11-28 13:23:49 +05:00
parent 10ec7fb33b
commit 7c4ed6a16c
6 changed files with 5 additions and 899 deletions

1
DELETE_wordpress_api.txt Normal file
View File

@@ -0,0 +1 @@
# This file has been removed to fix circular import issues

View File

@@ -1,400 +0,0 @@
"""
WordPress Publishing API Views
Handles manual content publishing to WordPress sites
"""
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from django.utils import timezone
from typing import Dict, Any, List
from igny8_core.models import ContentPost, SiteIntegration
from igny8_core.tasks.wordpress_publishing import (
publish_content_to_wordpress,
bulk_publish_content_to_wordpress
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def publish_single_content(request, content_id: int) -> Response:
"""
Publish a single content item to WordPress
POST /api/v1/content/{content_id}/publish-to-wordpress/
Body:
{
"site_integration_id": 123, // Optional - will use default if not provided
"force": false // Optional - force republish even if already published
}
"""
try:
content = get_object_or_404(ContentPost, id=content_id)
# Check permissions
if not request.user.has_perm('content.change_contentpost'):
return Response(
{
'success': False,
'message': 'Permission denied',
'error': 'insufficient_permissions'
},
status=status.HTTP_403_FORBIDDEN
)
# Get site integration
site_integration_id = request.data.get('site_integration_id')
force = request.data.get('force', False)
if site_integration_id:
site_integration = get_object_or_404(SiteIntegration, id=site_integration_id)
else:
# Get default WordPress integration for user's organization
site_integration = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True,
# Add organization filter if applicable
).first()
if not site_integration:
return Response(
{
'success': False,
'message': 'No WordPress integration found',
'error': 'no_integration'
},
status=status.HTTP_400_BAD_REQUEST
)
# Check if already published (unless force is true)
if not force and content.wordpress_sync_status == 'success':
return Response(
{
'success': True,
'message': 'Content already published to WordPress',
'data': {
'content_id': content.id,
'wordpress_post_id': content.wordpress_post_id,
'wordpress_post_url': content.wordpress_post_url,
'status': 'already_published'
}
}
)
# Check if currently syncing
if content.wordpress_sync_status == 'syncing':
return Response(
{
'success': False,
'message': 'Content is currently being published to WordPress',
'error': 'sync_in_progress'
},
status=status.HTTP_409_CONFLICT
)
# Validate content is ready for publishing
if not content.title or not (content.content_html or content.content):
return Response(
{
'success': False,
'message': 'Content is incomplete - missing title or content',
'error': 'incomplete_content'
},
status=status.HTTP_400_BAD_REQUEST
)
# Set status to pending and queue the task
content.wordpress_sync_status = 'pending'
content.save(update_fields=['wordpress_sync_status'])
# Get task_id if content is associated with a writer task
task_id = None
if hasattr(content, 'writer_task'):
task_id = content.writer_task.id
# Queue the publishing task
task_result = publish_content_to_wordpress.delay(
content.id,
site_integration.id,
task_id
)
return Response(
{
'success': True,
'message': 'Content queued for WordPress publishing',
'data': {
'content_id': content.id,
'site_integration_id': site_integration.id,
'task_id': task_result.id,
'status': 'queued'
}
},
status=status.HTTP_202_ACCEPTED
)
except Exception as e:
return Response(
{
'success': False,
'message': f'Error queuing content for WordPress publishing: {str(e)}',
'error': 'server_error'
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def bulk_publish_content(request) -> Response:
"""
Bulk publish multiple content items to WordPress
POST /api/v1/content/bulk-publish-to-wordpress/
Body:
{
"content_ids": [1, 2, 3, 4],
"site_integration_id": 123, // Optional
"force": false // Optional
}
"""
try:
content_ids = request.data.get('content_ids', [])
site_integration_id = request.data.get('site_integration_id')
force = request.data.get('force', False)
if not content_ids:
return Response(
{
'success': False,
'message': 'No content IDs provided',
'error': 'missing_content_ids'
},
status=status.HTTP_400_BAD_REQUEST
)
# Check permissions
if not request.user.has_perm('content.change_contentpost'):
return Response(
{
'success': False,
'message': 'Permission denied',
'error': 'insufficient_permissions'
},
status=status.HTTP_403_FORBIDDEN
)
# Get site integration
if site_integration_id:
site_integration = get_object_or_404(SiteIntegration, id=site_integration_id)
else:
site_integration = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True,
).first()
if not site_integration:
return Response(
{
'success': False,
'message': 'No WordPress integration found',
'error': 'no_integration'
},
status=status.HTTP_400_BAD_REQUEST
)
# Validate content items
content_items = ContentPost.objects.filter(id__in=content_ids)
if content_items.count() != len(content_ids):
return Response(
{
'success': False,
'message': 'Some content items not found',
'error': 'content_not_found'
},
status=status.HTTP_404_NOT_FOUND
)
# Queue bulk publishing task
task_result = bulk_publish_content_to_wordpress.delay(
content_ids,
site_integration.id
)
return Response(
{
'success': True,
'message': f'{len(content_ids)} content items queued for WordPress publishing',
'data': {
'content_count': len(content_ids),
'site_integration_id': site_integration.id,
'task_id': task_result.id,
'status': 'queued'
}
},
status=status.HTTP_202_ACCEPTED
)
except Exception as e:
return Response(
{
'success': False,
'message': f'Error queuing bulk WordPress publishing: {str(e)}',
'error': 'server_error'
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_wordpress_status(request, content_id: int) -> Response:
"""
Get WordPress publishing status for a content item
GET /api/v1/content/{content_id}/wordpress-status/
"""
try:
content = get_object_or_404(ContentPost, id=content_id)
return Response(
{
'success': True,
'data': {
'content_id': content.id,
'wordpress_sync_status': content.wordpress_sync_status,
'wordpress_post_id': content.wordpress_post_id,
'wordpress_post_url': content.wordpress_post_url,
'wordpress_sync_attempts': content.wordpress_sync_attempts,
'last_wordpress_sync': content.last_wordpress_sync.isoformat() if content.last_wordpress_sync else None,
}
}
)
except Exception as e:
return Response(
{
'success': False,
'message': f'Error getting WordPress status: {str(e)}',
'error': 'server_error'
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_wordpress_integrations(request) -> Response:
"""
Get available WordPress integrations for publishing
GET /api/v1/wordpress-integrations/
"""
try:
integrations = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True,
# Add organization filter if applicable
).values(
'id', 'site_name', 'site_url', 'is_active',
'created_at', 'last_sync_at'
)
return Response(
{
'success': True,
'data': list(integrations)
}
)
except Exception as e:
return Response(
{
'success': False,
'message': f'Error getting WordPress integrations: {str(e)}',
'error': 'server_error'
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def retry_failed_wordpress_sync(request, content_id: int) -> Response:
"""
Retry a failed WordPress sync
POST /api/v1/content/{content_id}/retry-wordpress-sync/
"""
try:
content = get_object_or_404(ContentPost, id=content_id)
if content.wordpress_sync_status != 'failed':
return Response(
{
'success': False,
'message': 'Content is not in failed status',
'error': 'invalid_status'
},
status=status.HTTP_400_BAD_REQUEST
)
# Get default WordPress integration
site_integration = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True,
).first()
if not site_integration:
return Response(
{
'success': False,
'message': 'No WordPress integration found',
'error': 'no_integration'
},
status=status.HTTP_400_BAD_REQUEST
)
# Reset status and retry
content.wordpress_sync_status = 'pending'
content.save(update_fields=['wordpress_sync_status'])
# Get task_id if available
task_id = None
if hasattr(content, 'writer_task'):
task_id = content.writer_task.id
# Queue the publishing task
task_result = publish_content_to_wordpress.delay(
content.id,
site_integration.id,
task_id
)
return Response(
{
'success': True,
'message': 'WordPress sync retry queued',
'data': {
'content_id': content.id,
'task_id': task_result.id,
'status': 'queued'
}
},
status=status.HTTP_202_ACCEPTED
)
except Exception as e:
return Response(
{
'success': False,
'message': f'Error retrying WordPress sync: {str(e)}',
'error': 'server_error'
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

View File

@@ -1,385 +0,0 @@
"""
IGNY8 Content Publishing Celery Tasks
Handles automated publishing of content from IGNY8 to WordPress sites.
"""
from celery import shared_task
from django.conf import settings
from django.utils import timezone
from datetime import timedelta
import requests
import logging
from typing import Dict, List, Any, Optional
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3)
def publish_content_to_wordpress(self, content_id: int, site_integration_id: int, task_id: Optional[int] = None) -> Dict[str, Any]:
"""
Publish a single content item to WordPress
Args:
content_id: IGNY8 content ID
site_integration_id: WordPress site integration ID
task_id: Optional IGNY8 task ID
Returns:
Dict with success status and details
"""
try:
from igny8_core.models import ContentPost, SiteIntegration
# Get content and site integration
try:
content = ContentPost.objects.get(id=content_id)
site_integration = SiteIntegration.objects.get(id=site_integration_id)
except (ContentPost.DoesNotExist, SiteIntegration.DoesNotExist) as e:
logger.error(f"Content or site integration not found: {e}")
return {"success": False, "error": str(e)}
# Check if content is ready for publishing
if content.wordpress_sync_status == 'success':
logger.info(f"Content {content_id} already published to WordPress")
return {"success": True, "message": "Already published", "wordpress_post_id": content.wordpress_post_id}
if content.wordpress_sync_status == 'syncing':
logger.info(f"Content {content_id} is currently syncing")
return {"success": False, "error": "Content is currently syncing"}
# Update status to syncing
content.wordpress_sync_status = 'syncing'
content.save(update_fields=['wordpress_sync_status'])
# Prepare content data for WordPress
content_data = {
'content_id': content.id,
'task_id': task_id,
'title': content.title,
'content_html': content.content_html or content.content,
'excerpt': content.brief or '',
'status': 'publish',
'author_email': content.author.email if content.author else None,
'author_name': content.author.get_full_name() if content.author else None,
'published_at': content.published_at.isoformat() if content.published_at else None,
'seo_title': getattr(content, 'seo_title', ''),
'seo_description': getattr(content, 'seo_description', ''),
'featured_image_url': content.featured_image.url if content.featured_image else None,
'sectors': [{'id': s.id, 'name': s.name} for s in content.sectors.all()],
'clusters': [{'id': c.id, 'name': c.name} for c in content.clusters.all()],
'tags': getattr(content, 'tags', []),
'focus_keywords': getattr(content, 'focus_keywords', [])
}
# Call WordPress REST API
wordpress_url = f"{site_integration.site_url}/wp-json/igny8/v1/publish-content/"
headers = {
'Content-Type': 'application/json',
'X-IGNY8-API-KEY': site_integration.api_key,
}
response = requests.post(
wordpress_url,
json=content_data,
headers=headers,
timeout=30
)
if response.status_code == 201:
# Success
wp_data = response.json().get('data', {})
content.wordpress_sync_status = 'success'
content.wordpress_post_id = wp_data.get('post_id')
content.wordpress_post_url = wp_data.get('post_url')
content.last_wordpress_sync = timezone.now()
content.save(update_fields=[
'wordpress_sync_status', 'wordpress_post_id',
'wordpress_post_url', 'last_wordpress_sync'
])
logger.info(f"Successfully published content {content_id} to WordPress post {content.wordpress_post_id}")
return {
"success": True,
"wordpress_post_id": content.wordpress_post_id,
"wordpress_post_url": content.wordpress_post_url
}
elif response.status_code == 409:
# Content already exists
wp_data = response.json().get('data', {})
content.wordpress_sync_status = 'success'
content.wordpress_post_id = wp_data.get('post_id')
content.last_wordpress_sync = timezone.now()
content.save(update_fields=[
'wordpress_sync_status', 'wordpress_post_id', 'last_wordpress_sync'
])
logger.info(f"Content {content_id} already exists on WordPress")
return {"success": True, "message": "Content already exists", "wordpress_post_id": content.wordpress_post_id}
else:
# Error
error_msg = f"WordPress API error: {response.status_code} - {response.text}"
logger.error(error_msg)
# Retry logic
if self.request.retries < self.max_retries:
content.wordpress_sync_attempts = (content.wordpress_sync_attempts or 0) + 1
content.save(update_fields=['wordpress_sync_attempts'])
# Exponential backoff: 1min, 5min, 15min
countdown = 60 * (5 ** self.request.retries)
raise self.retry(countdown=countdown, exc=Exception(error_msg))
else:
# Max retries reached
content.wordpress_sync_status = 'failed'
content.last_wordpress_sync = timezone.now()
content.save(update_fields=['wordpress_sync_status', 'last_wordpress_sync'])
return {"success": False, "error": error_msg}
except Exception as e:
logger.error(f"Error publishing content {content_id}: {str(e)}")
# Update content status on error
try:
content = ContentPost.objects.get(id=content_id)
content.wordpress_sync_status = 'failed'
content.last_wordpress_sync = timezone.now()
content.save(update_fields=['wordpress_sync_status', 'last_wordpress_sync'])
except:
pass
return {"success": False, "error": str(e)}
@shared_task
def process_pending_wordpress_publications() -> Dict[str, Any]:
"""
Process all content items pending WordPress publication
Runs every 5 minutes
"""
try:
from igny8_core.models import ContentPost, SiteIntegration
# Find content marked for WordPress publishing
pending_content = ContentPost.objects.filter(
wordpress_sync_status='pending',
published_at__isnull=False # Only published content
).select_related('author').prefetch_related('sectors', 'clusters')
if not pending_content.exists():
logger.info("No content pending WordPress publication")
return {"success": True, "processed": 0}
# Get active WordPress integrations
active_integrations = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True,
api_key__isnull=False
)
if not active_integrations.exists():
logger.warning("No active WordPress integrations found")
return {"success": False, "error": "No active WordPress integrations"}
processed = 0
failed = 0
for content in pending_content[:50]: # Process max 50 at a time
for integration in active_integrations:
# Get task_id if content is associated with a task
task_id = None
if hasattr(content, 'writer_task'):
task_id = content.writer_task.id
# Queue individual publish task
publish_content_to_wordpress.delay(
content.id,
integration.id,
task_id
)
processed += 1
logger.info(f"Queued {processed} content items for WordPress publication")
return {"success": True, "processed": processed, "failed": failed}
except Exception as e:
logger.error(f"Error processing pending WordPress publications: {str(e)}")
return {"success": False, "error": str(e)}
@shared_task
def bulk_publish_content_to_wordpress(content_ids: List[int], site_integration_id: int) -> Dict[str, Any]:
"""
Bulk publish multiple content items to WordPress
Used for manual bulk operations from Content Manager
"""
try:
from igny8_core.models import ContentPost, SiteIntegration
site_integration = SiteIntegration.objects.get(id=site_integration_id)
content_items = ContentPost.objects.filter(id__in=content_ids)
results = {
"success": True,
"total": len(content_ids),
"queued": 0,
"skipped": 0,
"errors": []
}
for content in content_items:
try:
# Skip if already published or syncing
if content.wordpress_sync_status in ['success', 'syncing']:
results["skipped"] += 1
continue
# Mark as pending and queue
content.wordpress_sync_status = 'pending'
content.save(update_fields=['wordpress_sync_status'])
# Get task_id if available
task_id = None
if hasattr(content, 'writer_task'):
task_id = content.writer_task.id
# Queue individual publish task
publish_content_to_wordpress.delay(
content.id,
site_integration.id,
task_id
)
results["queued"] += 1
except Exception as e:
results["errors"].append(f"Content {content.id}: {str(e)}")
if results["errors"]:
results["success"] = len(results["errors"]) < results["total"] / 2 # Success if < 50% errors
logger.info(f"Bulk publish: {results['queued']} queued, {results['skipped']} skipped, {len(results['errors'])} errors")
return results
except Exception as e:
logger.error(f"Error in bulk publish: {str(e)}")
return {"success": False, "error": str(e)}
@shared_task
def wordpress_status_reconciliation() -> Dict[str, Any]:
"""
Daily task to reconcile status between IGNY8 and WordPress
Checks for discrepancies and fixes them
"""
try:
from igny8_core.models import ContentPost, SiteIntegration
# Get content marked as published to WordPress
wp_content = ContentPost.objects.filter(
wordpress_sync_status='success',
wordpress_post_id__isnull=False
)
active_integrations = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True
)
reconciled = 0
errors = []
for integration in active_integrations:
integration_content = wp_content.filter(
# Assuming there's a way to link content to integration
# This would depend on your data model
)
for content in integration_content[:100]: # Limit to prevent timeouts
try:
# Check WordPress post status
wp_url = f"{integration.site_url}/wp-json/igny8/v1/post-status/{content.id}/"
headers = {'X-IGNY8-API-KEY': integration.api_key}
response = requests.get(wp_url, headers=headers, timeout=10)
if response.status_code == 200:
wp_data = response.json().get('data', {})
wp_status = wp_data.get('wordpress_status')
# Update if status changed
if wp_status == 'trash' and content.wordpress_sync_status == 'success':
content.wordpress_sync_status = 'failed'
content.save(update_fields=['wordpress_sync_status'])
reconciled += 1
elif response.status_code == 404:
# Post not found on WordPress
content.wordpress_sync_status = 'failed'
content.wordpress_post_id = None
content.wordpress_post_url = None
content.save(update_fields=[
'wordpress_sync_status', 'wordpress_post_id', 'wordpress_post_url'
])
reconciled += 1
except Exception as e:
errors.append(f"Content {content.id}: {str(e)}")
logger.info(f"Status reconciliation: {reconciled} reconciled, {len(errors)} errors")
return {"success": True, "reconciled": reconciled, "errors": errors}
except Exception as e:
logger.error(f"Error in status reconciliation: {str(e)}")
return {"success": False, "error": str(e)}
@shared_task
def retry_failed_wordpress_publications() -> Dict[str, Any]:
"""
Retry failed WordPress publications (runs daily)
Only retries items that failed more than 1 hour ago
"""
try:
from igny8_core.models import ContentPost, SiteIntegration
# Find failed publications older than 1 hour
one_hour_ago = timezone.now() - timedelta(hours=1)
failed_content = ContentPost.objects.filter(
wordpress_sync_status='failed',
last_wordpress_sync__lt=one_hour_ago,
wordpress_sync_attempts__lt=5 # Max 5 total attempts
)
active_integrations = SiteIntegration.objects.filter(
platform='wordpress',
is_active=True
)
retried = 0
for content in failed_content[:20]: # Limit retries per run
for integration in active_integrations:
# Reset status and retry
content.wordpress_sync_status = 'pending'
content.save(update_fields=['wordpress_sync_status'])
task_id = None
if hasattr(content, 'writer_task'):
task_id = content.writer_task.id
publish_content_to_wordpress.delay(
content.id,
integration.id,
task_id
)
retried += 1
break # Only retry with first active integration
logger.info(f"Retried {retried} failed WordPress publications")
return {"success": True, "retried": retried}
except Exception as e:
logger.error(f"Error retrying failed publications: {str(e)}")
return {"success": False, "error": str(e)}

View File

@@ -1,38 +0,0 @@
"""
URL configuration for WordPress publishing endpoints
"""
from django.urls import path
from igny8_core.api.wordpress_publishing import (
publish_single_content,
bulk_publish_content,
get_wordpress_status,
get_wordpress_integrations,
retry_failed_wordpress_sync,
)
urlpatterns = [
# Single content publishing
path('content/<int:content_id>/publish-to-wordpress/',
publish_single_content,
name='publish_single_content'),
# Bulk content publishing
path('content/bulk-publish-to-wordpress/',
bulk_publish_content,
name='bulk_publish_content'),
# WordPress status
path('content/<int:content_id>/wordpress-status/',
get_wordpress_status,
name='get_wordpress_status'),
# WordPress integrations list
path('wordpress-integrations/',
get_wordpress_integrations,
name='get_wordpress_integrations'),
# Retry failed sync
path('content/<int:content_id>/retry-wordpress-sync/',
retry_failed_wordpress_sync,
name='retry_failed_wordpress_sync'),
]

View File

@@ -321,16 +321,6 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
},
'/writer/images': {
rowActions: [
{
key: 'publish_wordpress',
label: 'Publish to WordPress',
icon: <ArrowRightIcon className="w-5 h-5" />,
variant: 'success',
shouldShow: (row: any) => {
// Only show if images are generated (complete) - WordPress status is tracked separately
return row.overall_status === 'complete';
},
},
{
key: 'update_status',
label: 'Update Status',
@@ -338,14 +328,7 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
variant: 'primary',
},
],
bulkActions: [
{
key: 'bulk_publish_wordpress',
label: 'Publish Ready to WordPress',
icon: <ArrowRightIcon className="w-5 h-5" />,
variant: 'success',
},
],
bulkActions: [],
},
// Default config (fallback)
default: {

View File

@@ -14,7 +14,6 @@ import {
bulkUpdateImagesStatus,
ContentImage,
fetchAPI,
publishContent,
} from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
@@ -208,50 +207,8 @@ export default function Images() {
// Bulk action handler
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'bulk_publish_wordpress') {
// Filter to only publish items that have images generated
const readyItems = images
.filter(item => ids.includes(item.content_id.toString()))
.filter(item => item.overall_status === 'complete');
if (readyItems.length === 0) {
toast.warning('No items are ready for WordPress publishing. Items must have generated images and not already be published.');
return;
}
try {
let successCount = 0;
let failedCount = 0;
const errors: string[] = [];
// Process each item individually using the existing publishContent function
for (const item of readyItems) {
try {
await publishContent(item.content_id);
successCount++;
} catch (error: any) {
failedCount++;
errors.push(`${item.content_title}: ${error.message}`);
}
}
if (successCount > 0) {
toast.success(`Successfully published ${successCount} item(s) to WordPress`);
}
if (failedCount > 0) {
toast.warning(`${failedCount} item(s) failed to publish`);
}
// Reload images to reflect the updated WordPress status
loadImages();
} catch (error: any) {
console.error('Bulk WordPress publish error:', error);
toast.error(`Failed to bulk publish to WordPress: ${error.message || 'Network error'}`);
}
} else {
toast.info(`Bulk action "${action}" for ${ids.length} items`);
}
}, [images, toast, loadImages]);
toast.info(`Bulk action "${action}" for ${ids.length} items`);
}, [toast]);
// Row action handler
const handleRowAction = useCallback(async (action: string, row: ContentImagesGroup) => {
@@ -259,20 +216,8 @@ export default function Images() {
setStatusUpdateContentId(row.content_id);
setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`);
setIsStatusModalOpen(true);
} else if (action === 'publish_wordpress') {
// Handle WordPress publishing for individual item using existing publishContent function
try {
// Use the existing publishContent function from the API
const result = await publishContent(row.content_id);
toast.success(`Successfully published "${row.content_title}" to WordPress! View at: ${result.external_url}`);
// Reload images to reflect the updated WordPress status
loadImages();
} catch (error: any) {
console.error('WordPress publish error:', error);
toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`);
}
}
}, [loadImages, toast]);
}, []);
// Handle status update confirmation
const handleStatusUpdate = useCallback(async (status: string) => {