fina autoamtiona adn billing and credits

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 15:54:15 +00:00
parent f8a9293196
commit 40dfe20ead
40 changed files with 5680 additions and 18 deletions

View File

@@ -0,0 +1,390 @@
/**
* Current Processing Card Component
* Shows real-time automation progress with pause/resume/cancel controls
*/
import React, { useEffect, useState } from 'react';
import { automationService, ProcessingState, AutomationRun } from '../../services/automationService';
import { useToast } from '../ui/toast/ToastContainer';
import Button from '../ui/button/Button';
import {
PlayIcon,
PauseIcon,
XMarkIcon,
ClockIcon,
BoltIcon
} from '../../icons';
interface CurrentProcessingCardProps {
runId: string;
siteId: number;
currentRun: AutomationRun;
onUpdate: () => void;
onClose: () => void;
}
const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
runId,
siteId,
currentRun,
onUpdate,
onClose,
}) => {
const [processingState, setProcessingState] = useState<ProcessingState | null>(null);
const [error, setError] = useState<string | null>(null);
const [isPausing, setIsPausing] = useState(false);
const [isResuming, setIsResuming] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
const toast = useToast();
useEffect(() => {
let isMounted = true;
const fetchState = async () => {
try {
const state = await automationService.getCurrentProcessing(siteId, runId);
if (!isMounted) return;
setProcessingState(state);
setError(null);
// If stage completed (all items processed), trigger page refresh
if (state && state.processed_items >= state.total_items && state.total_items > 0) {
onUpdate();
}
} catch (err) {
if (!isMounted) return;
console.error('Error fetching processing state:', err);
setError('Failed to load processing state');
}
};
// Only fetch if status is running or paused
if (currentRun.status === 'running' || currentRun.status === 'paused') {
// Initial fetch
fetchState();
// Poll every 3 seconds
const interval = setInterval(fetchState, 3000);
return () => {
isMounted = false;
clearInterval(interval);
};
}
return () => {
isMounted = false;
};
}, [siteId, runId, currentRun.status, onUpdate]);
const handlePause = async () => {
setIsPausing(true);
try {
await automationService.pause(siteId, runId);
toast?.success('Automation pausing... will complete current item');
// Trigger update to refresh run status
setTimeout(onUpdate, 1000);
} catch (error: any) {
toast?.error(error?.message || 'Failed to pause automation');
} finally {
setIsPausing(false);
}
};
const handleResume = async () => {
setIsResuming(true);
try {
await automationService.resume(siteId, runId);
toast?.success('Automation resumed');
// Trigger update to refresh run status
setTimeout(onUpdate, 1000);
} catch (error: any) {
toast?.error(error?.message || 'Failed to resume automation');
} finally {
setIsResuming(false);
}
};
const handleCancel = async () => {
if (!confirm('Are you sure you want to cancel this automation run? This cannot be undone.')) {
return;
}
setIsCancelling(true);
try {
await automationService.cancel(siteId, runId);
toast?.success('Automation cancelling... will complete current item');
// Trigger update to refresh run status
setTimeout(onUpdate, 1500);
} catch (error: any) {
toast?.error(error?.message || 'Failed to cancel automation');
} finally {
setIsCancelling(false);
}
};
const formatDuration = (startTime: string) => {
const start = new Date(startTime).getTime();
const now = Date.now();
const diffMs = now - start;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
if (diffHours > 0) {
return `${diffHours}h ${diffMins % 60}m`;
}
return `${diffMins}m`;
};
if (error) {
return (
<div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-500 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between">
<p className="text-red-700 dark:text-red-300 text-sm">{error}</p>
<button
onClick={onClose}
className="text-red-500 hover:text-red-700 dark:hover:text-red-300"
>
<XMarkIcon className="w-5 h-5" />
</button>
</div>
</div>
);
}
if (!processingState && currentRun.status === 'running') {
return null;
}
const percentage = processingState?.percentage || 0;
const isPaused = currentRun.status === 'paused';
return (
<div className={`border-2 rounded-lg p-6 mb-6 ${
isPaused
? 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500'
: 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
}`}>
{/* Header Row with Main Info and Close */}
<div className="flex items-start justify-between mb-4">
{/* Left Side - Main Info (75%) */}
<div className="flex-1 pr-6">
<div className="flex items-center gap-3 mb-4">
<div className={isPaused ? '' : 'animate-pulse'}>
{isPaused ? (
<PauseIcon className="w-8 h-8 text-yellow-600 dark:text-yellow-400" />
) : (
<BoltIcon className="w-8 h-8 text-blue-600 dark:text-blue-400" />
)}
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
</h2>
{processingState && (
<p className="text-sm text-gray-600 dark:text-gray-400">
Stage {currentRun.current_stage}: {processingState.stage_name}
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
isPaused
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
}`}>
{processingState.stage_type}
</span>
</p>
)}
</div>
</div>
{/* Progress Info */}
{processingState && (
<>
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{percentage}%
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{processingState.processed_items}/{processingState.total_items} completed
</div>
</div>
{/* Progress Bar */}
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all duration-500 ${
isPaused
? 'bg-yellow-600 dark:bg-yellow-500'
: 'bg-blue-600 dark:bg-blue-500'
}`}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
</div>
</div>
{/* Currently Processing and Up Next */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* Currently Processing */}
<div>
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Currently Processing:
</h3>
<div className="space-y-1">
{processingState.currently_processing.length > 0 ? (
processingState.currently_processing.map((item, idx) => (
<div key={idx} className="flex items-start gap-2 text-sm">
<span className={isPaused ? 'text-yellow-600 dark:text-yellow-400 mt-1' : 'text-blue-600 dark:text-blue-400 mt-1'}></span>
<span className="text-gray-800 dark:text-gray-200 font-medium line-clamp-2">
{item.title}
</span>
</div>
))
) : (
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
{isPaused ? 'Paused' : 'No items currently processing'}
</div>
)}
</div>
</div>
{/* Up Next */}
<div>
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Up Next:
</h3>
<div className="space-y-1">
{processingState.up_next.length > 0 ? (
<>
{processingState.up_next.map((item, idx) => (
<div key={idx} className="flex items-start gap-2 text-sm">
<span className="text-gray-400 dark:text-gray-500 mt-1"></span>
<span className="text-gray-600 dark:text-gray-400 line-clamp-2">
{item.title}
</span>
</div>
))}
{processingState.remaining_count > processingState.up_next.length + processingState.currently_processing.length && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
+ {processingState.remaining_count - processingState.up_next.length - processingState.currently_processing.length} more in queue
</div>
)}
</>
) : (
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
Queue empty
</div>
)}
</div>
</div>
</div>
{/* Control Buttons */}
<div className="flex items-center gap-3">
{currentRun.status === 'running' ? (
<Button
onClick={handlePause}
disabled={isPausing}
variant="secondary"
size="sm"
>
<PauseIcon className="w-4 h-4 mr-2" />
{isPausing ? 'Pausing...' : 'Pause'}
</Button>
) : currentRun.status === 'paused' ? (
<Button
onClick={handleResume}
disabled={isResuming}
variant="primary"
size="sm"
>
<PlayIcon className="w-4 h-4 mr-2" />
{isResuming ? 'Resuming...' : 'Resume'}
</Button>
) : null}
<Button
onClick={handleCancel}
disabled={isCancelling}
variant="danger"
size="sm"
>
<XMarkIcon className="w-4 h-4 mr-2" />
{isCancelling ? 'Cancelling...' : 'Cancel'}
</Button>
</div>
</>
)}
</div>
{/* Right Side - Metrics and Close (25%) */}
<div className="w-64 flex-shrink-0">
{/* Close Button */}
<div className="flex justify-end mb-4">
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
title="Close (card will remain available below)"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
{/* Metrics Cards */}
<div className="space-y-3">
{/* Duration */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 mb-1">
<ClockIcon className="w-4 h-4 text-gray-500" />
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
Duration
</div>
</div>
<div className="text-xl font-bold text-gray-900 dark:text-white">
{formatDuration(currentRun.started_at)}
</div>
</div>
{/* Credits Used */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 mb-1">
<BoltIcon className="w-4 h-4 text-amber-500" />
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
Credits Used
</div>
</div>
<div className="text-xl font-bold text-amber-600 dark:text-amber-400">
{currentRun.total_credits_used}
</div>
</div>
{/* Current Stage */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
Stage
</div>
<div className="text-xl font-bold text-gray-900 dark:text-white">
{currentRun.current_stage} of 7
</div>
</div>
{/* Status */}
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
Status
</div>
<div className={`text-sm font-semibold ${
isPaused
? 'text-yellow-600 dark:text-yellow-400'
: 'text-blue-600 dark:text-blue-400'
}`}>
{isPaused ? 'Paused' : 'Running'}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default CurrentProcessingCard;