old automation cleanup adn status feilds of planner udpate
This commit is contained in:
@@ -64,11 +64,6 @@ const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
|
||||
// Setup Pages - Lazy loaded
|
||||
const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSectorsKeywords"));
|
||||
|
||||
// Other Pages - Lazy loaded
|
||||
const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard"));
|
||||
const AutomationRules = lazy(() => import("./pages/Automation/Rules"));
|
||||
const AutomationTasks = lazy(() => import("./pages/Automation/Tasks"));
|
||||
|
||||
// Settings - Lazy loaded
|
||||
const GeneralSettings = lazy(() => import("./pages/Settings/General"));
|
||||
const Users = lazy(() => import("./pages/Settings/Users"));
|
||||
@@ -364,23 +359,6 @@ export default function App() {
|
||||
{/* Legacy redirect */}
|
||||
<Route path="/setup/industries-sectors-keywords" element={<Navigate to="/setup/add-keywords" replace />} />
|
||||
|
||||
{/* Automation Module - Redirect dashboard to rules */}
|
||||
<Route path="/automation" element={<Navigate to="/automation/rules" replace />} />
|
||||
<Route path="/automation/rules" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="automation">
|
||||
<AutomationRules />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/automation/tasks" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="automation">
|
||||
<AutomationTasks />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="/settings" element={
|
||||
<Suspense fallback={null}>
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { fetchAPI } from '../services/api';
|
||||
|
||||
/**
|
||||
* Automation API Client
|
||||
* Functions for automation rules and scheduled tasks
|
||||
*/
|
||||
|
||||
export interface AutomationRule {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
trigger: 'schedule' | 'event' | 'manual';
|
||||
schedule?: string; // Cron-like string
|
||||
conditions: Array<{
|
||||
field: string;
|
||||
operator: string;
|
||||
value: any;
|
||||
}>;
|
||||
actions: Array<{
|
||||
type: string;
|
||||
params: Record<string, any>;
|
||||
}>;
|
||||
is_active: boolean;
|
||||
status: 'active' | 'inactive' | 'paused';
|
||||
execution_count: number;
|
||||
last_executed_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ScheduledTask {
|
||||
id: number;
|
||||
rule_id?: number;
|
||||
rule_name?: string;
|
||||
task_type: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
scheduled_at: string;
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
result?: Record<string, any>;
|
||||
error?: string;
|
||||
retry_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AutomationRuleCreateData {
|
||||
name: string;
|
||||
description?: string;
|
||||
trigger: 'schedule' | 'event' | 'manual';
|
||||
schedule?: string;
|
||||
conditions?: Array<{
|
||||
field: string;
|
||||
operator: string;
|
||||
value: any;
|
||||
}>;
|
||||
actions: Array<{
|
||||
type: string;
|
||||
params: Record<string, any>;
|
||||
}>;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface AutomationRuleUpdateData extends Partial<AutomationRuleCreateData> {}
|
||||
|
||||
export const automationApi = {
|
||||
/**
|
||||
* List automation rules
|
||||
*/
|
||||
listRules: async (filters?: {
|
||||
search?: string;
|
||||
trigger?: string;
|
||||
is_active?: boolean;
|
||||
status?: string;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.trigger) params.append('trigger', filters.trigger);
|
||||
if (filters?.is_active !== undefined) params.append('is_active', String(filters.is_active));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.ordering) params.append('ordering', filters.ordering);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.page_size) params.append('page_size', String(filters.page_size));
|
||||
|
||||
const query = params.toString();
|
||||
return await fetchAPI(`/v1/automation/rules/${query ? `?${query}` : ''}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single automation rule
|
||||
*/
|
||||
getRule: async (id: number) => {
|
||||
return await fetchAPI(`/v1/automation/rules/${id}/`) as AutomationRule;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new automation rule
|
||||
*/
|
||||
createRule: async (data: AutomationRuleCreateData) => {
|
||||
return await fetchAPI('/v1/automation/rules/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}) as AutomationRule;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an automation rule
|
||||
*/
|
||||
updateRule: async (id: number, data: AutomationRuleUpdateData) => {
|
||||
return await fetchAPI(`/v1/automation/rules/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
}) as AutomationRule;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an automation rule
|
||||
*/
|
||||
deleteRule: async (id: number) => {
|
||||
return await fetchAPI(`/v1/automation/rules/${id}/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute an automation rule manually
|
||||
*/
|
||||
executeRule: async (id: number, context?: Record<string, any>) => {
|
||||
return await fetchAPI(`/v1/automation/rules/${id}/execute/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ context: context || {} }),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* List scheduled tasks
|
||||
*/
|
||||
listTasks: async (filters?: {
|
||||
rule_id?: number;
|
||||
status?: string;
|
||||
task_type?: string;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.rule_id) params.append('rule_id', String(filters.rule_id));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.task_type) params.append('task_type', filters.task_type);
|
||||
if (filters?.ordering) params.append('ordering', filters.ordering);
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.page_size) params.append('page_size', String(filters.page_size));
|
||||
|
||||
const query = params.toString();
|
||||
return await fetchAPI(`/v1/automation/scheduled-tasks/${query ? `?${query}` : ''}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single scheduled task
|
||||
*/
|
||||
getTask: async (id: number) => {
|
||||
return await fetchAPI(`/v1/automation/scheduled-tasks/${id}/`) as ScheduledTask;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry a failed scheduled task
|
||||
*/
|
||||
retryTask: async (id: number) => {
|
||||
return await fetchAPI(`/v1/automation/scheduled-tasks/${id}/retry/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -192,7 +192,7 @@ export const createClustersPageConfig = (
|
||||
render: (value: string) => {
|
||||
const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-';
|
||||
return (
|
||||
<Badge color={value === 'active' ? 'success' : 'warning'} size="xs" variant="soft">
|
||||
<Badge color={value === 'mapped' ? 'success' : 'amber'} size="xs" variant="soft">
|
||||
<span className="text-[11px] font-normal">{properCase}</span>
|
||||
</Badge>
|
||||
);
|
||||
@@ -248,8 +248,8 @@ export const createClustersPageConfig = (
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'mapped', label: 'Mapped' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -409,12 +409,12 @@ export const createClustersPageConfig = (
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
value: handlers.formData.status || 'active',
|
||||
value: handlers.formData.status || 'new',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, status: value }),
|
||||
options: [
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'mapped', label: 'Mapped' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -173,8 +173,8 @@ export const createIdeasPageConfig = (
|
||||
render: (value: string) => {
|
||||
const statusColors: Record<string, 'success' | 'amber' | 'info'> = {
|
||||
'new': 'amber',
|
||||
'scheduled': 'info',
|
||||
'published': 'success',
|
||||
'queued': 'info',
|
||||
'completed': 'success',
|
||||
};
|
||||
const properCase = value ? value.charAt(0).toUpperCase() + value.slice(1) : '-';
|
||||
return (
|
||||
@@ -222,8 +222,8 @@ export const createIdeasPageConfig = (
|
||||
options: [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'scheduled', label: 'Scheduled' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'queued', label: 'Queued' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -380,8 +380,8 @@ export const createIdeasPageConfig = (
|
||||
handlers.setFormData({ ...handlers.formData, status: value }),
|
||||
options: [
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'scheduled', label: 'Scheduled' },
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'queued', label: 'Queued' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -399,13 +399,13 @@ export const createIdeasPageConfig = (
|
||||
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length,
|
||||
},
|
||||
{
|
||||
label: 'Scheduled',
|
||||
label: 'Queued',
|
||||
value: 0,
|
||||
accentColor: 'blue' as const,
|
||||
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'scheduled').length,
|
||||
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'queued').length,
|
||||
},
|
||||
{
|
||||
label: 'Published',
|
||||
label: 'Completed',
|
||||
value: 0,
|
||||
accentColor: 'green' as const,
|
||||
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'published').length,
|
||||
|
||||
@@ -232,9 +232,9 @@ export const createKeywordsPageConfig = (
|
||||
return (
|
||||
<Badge
|
||||
color={
|
||||
value === 'active'
|
||||
value === 'mapped'
|
||||
? 'success'
|
||||
: value === 'pending'
|
||||
: value === 'new'
|
||||
? 'amber'
|
||||
: 'error'
|
||||
}
|
||||
@@ -312,9 +312,8 @@ export const createKeywordsPageConfig = (
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'mapped', label: 'Mapped' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -560,13 +559,12 @@ export const createKeywordsPageConfig = (
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
value: handlers.formData.status || 'pending',
|
||||
value: handlers.formData.status || 'new',
|
||||
onChange: (value: any) =>
|
||||
handlers.setFormData({ ...handlers.formData, status: value }),
|
||||
options: [
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'mapped', label: 'Mapped' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -95,15 +95,6 @@ const AppSidebar: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// Add Automation if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('automation')) {
|
||||
setupItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Automation",
|
||||
path: "/automation/rules", // Default to rules, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
|
||||
// Add Thinker if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('thinker')) {
|
||||
setupItems.push({
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
import { useEffect, useState, lazy, Suspense } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import { ProgressBar } from "../../components/ui/progress";
|
||||
import { ApexOptions } from "apexcharts";
|
||||
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
|
||||
import PageHeader from "../../components/common/PageHeader";
|
||||
|
||||
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
|
||||
|
||||
import {
|
||||
BoltIcon,
|
||||
ClockIcon,
|
||||
CheckCircleIcon,
|
||||
ArrowRightIcon,
|
||||
CalendarIcon,
|
||||
ListIcon,
|
||||
GroupIcon,
|
||||
FileTextIcon,
|
||||
ArrowUpIcon,
|
||||
ArrowDownIcon,
|
||||
PaperPlaneIcon,
|
||||
CloseIcon,
|
||||
FileIcon,
|
||||
} from "../../icons";
|
||||
import { useSiteStore } from "../../store/siteStore";
|
||||
import { useSectorStore } from "../../store/sectorStore";
|
||||
|
||||
interface AutomationStats {
|
||||
activeWorkflows: number;
|
||||
scheduledTasks: number;
|
||||
completedToday: number;
|
||||
successRate: number;
|
||||
automationCoverage: {
|
||||
keywords: boolean;
|
||||
clustering: boolean;
|
||||
ideas: boolean;
|
||||
tasks: boolean;
|
||||
content: boolean;
|
||||
images: boolean;
|
||||
publishing: boolean;
|
||||
};
|
||||
recentActivity: Array<{
|
||||
id: number;
|
||||
type: string;
|
||||
status: string;
|
||||
timestamp: Date;
|
||||
itemsProcessed: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function AutomationDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector } = useSectorStore();
|
||||
|
||||
const [stats, setStats] = useState<AutomationStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
||||
|
||||
// Mock data for now - will be replaced with real API calls
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
setStats({
|
||||
activeWorkflows: 3,
|
||||
scheduledTasks: 12,
|
||||
completedToday: 47,
|
||||
successRate: 94.5,
|
||||
automationCoverage: {
|
||||
keywords: true,
|
||||
clustering: true,
|
||||
ideas: true,
|
||||
tasks: false,
|
||||
content: true,
|
||||
images: true,
|
||||
publishing: false,
|
||||
},
|
||||
recentActivity: [
|
||||
{
|
||||
id: 1,
|
||||
type: "Content Generation",
|
||||
status: "completed",
|
||||
timestamp: new Date(Date.now() - 15 * 60 * 1000),
|
||||
itemsProcessed: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "Image Generation",
|
||||
status: "completed",
|
||||
timestamp: new Date(Date.now() - 45 * 60 * 1000),
|
||||
itemsProcessed: 8,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "Keyword Clustering",
|
||||
status: "completed",
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
itemsProcessed: 12,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setLastUpdated(new Date());
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [activeSite, activeSector]);
|
||||
|
||||
const automationWorkflows = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Full Pipeline Automation",
|
||||
description: "Keywords → Clusters → Ideas → Tasks → Content → Images → Publish",
|
||||
status: "active",
|
||||
schedule: "Every 6 hours",
|
||||
lastRun: "2 hours ago",
|
||||
nextRun: "4 hours",
|
||||
coverage: 85,
|
||||
icon: PaperPlaneIcon,
|
||||
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Writer Workflow",
|
||||
description: "Tasks → Content → Images → Publishing",
|
||||
status: "active",
|
||||
schedule: "Every 3 hours",
|
||||
lastRun: "1 hour ago",
|
||||
nextRun: "2 hours",
|
||||
coverage: 92,
|
||||
icon: FileTextIcon,
|
||||
color: "from-[var(--color-success)] to-[var(--color-success-dark)]",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Planner Workflow",
|
||||
description: "Keywords → Clusters → Ideas",
|
||||
status: "active",
|
||||
schedule: "Every 6 hours",
|
||||
lastRun: "3 hours ago",
|
||||
nextRun: "3 hours",
|
||||
coverage: 78,
|
||||
icon: ListIcon,
|
||||
color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
|
||||
},
|
||||
];
|
||||
|
||||
const automationSteps = [
|
||||
{
|
||||
step: "Keywords",
|
||||
enabled: true,
|
||||
description: "Auto-add keywords from opportunities",
|
||||
path: "/planner/keyword-opportunities",
|
||||
icon: ListIcon,
|
||||
},
|
||||
{
|
||||
step: "Clustering",
|
||||
enabled: true,
|
||||
description: "Auto-cluster keywords into groups",
|
||||
path: "/planner/clusters",
|
||||
icon: GroupIcon,
|
||||
},
|
||||
{
|
||||
step: "Ideas",
|
||||
enabled: true,
|
||||
description: "Auto-generate content ideas from clusters",
|
||||
path: "/planner/ideas",
|
||||
icon: BoltIcon,
|
||||
},
|
||||
{
|
||||
step: "Tasks",
|
||||
enabled: false,
|
||||
description: "Auto-create tasks from ideas",
|
||||
path: "/writer/tasks",
|
||||
icon: CheckCircleIcon,
|
||||
},
|
||||
{
|
||||
step: "Content",
|
||||
enabled: true,
|
||||
description: "Auto-generate content from tasks",
|
||||
path: "/writer/content",
|
||||
icon: FileTextIcon,
|
||||
},
|
||||
{
|
||||
step: "Images",
|
||||
enabled: true,
|
||||
description: "Auto-generate images for content",
|
||||
path: "/writer/images",
|
||||
icon: FileIcon,
|
||||
},
|
||||
{
|
||||
step: "Publishing",
|
||||
enabled: false,
|
||||
description: "Auto-publish content to WordPress",
|
||||
path: "/writer/published",
|
||||
icon: PaperPlaneIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const chartOptions: ApexOptions = {
|
||||
chart: {
|
||||
type: "line",
|
||||
height: 300,
|
||||
toolbar: { show: false },
|
||||
zoom: { enabled: false },
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: 3,
|
||||
},
|
||||
xaxis: {
|
||||
categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
labels: { style: { colors: "#6b7280" } },
|
||||
},
|
||||
yaxis: {
|
||||
labels: { style: { colors: "#6b7280" } },
|
||||
},
|
||||
legend: {
|
||||
position: "top",
|
||||
labels: { colors: "#6b7280" },
|
||||
},
|
||||
colors: ["var(--color-primary)", "var(--color-success)", "var(--color-purple)"],
|
||||
grid: {
|
||||
borderColor: "#e5e7eb",
|
||||
},
|
||||
};
|
||||
|
||||
const chartSeries = [
|
||||
{
|
||||
name: "Automated",
|
||||
data: [12, 19, 15, 25, 22, 18, 24],
|
||||
},
|
||||
{
|
||||
name: "Manual",
|
||||
data: [5, 8, 6, 10, 9, 7, 11],
|
||||
},
|
||||
{
|
||||
name: "Failed",
|
||||
data: [1, 2, 1, 2, 1, 2, 1],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Automation Dashboard - IGNY8" description="Manage and monitor automation workflows" />
|
||||
<PageHeader title="Automation Dashboard" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Key Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<EnhancedMetricCard
|
||||
title="Active Workflows"
|
||||
value={stats?.activeWorkflows || 0}
|
||||
icon={<BoltIcon className="size-6" />}
|
||||
trend={0}
|
||||
accentColor="purple"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Scheduled Tasks"
|
||||
value={stats?.scheduledTasks || 0}
|
||||
icon={<CalendarIcon className="size-6" />}
|
||||
trend={0}
|
||||
accentColor="blue"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Completed Today"
|
||||
value={stats?.completedToday || 0}
|
||||
icon={<CheckCircleIcon className="size-6" />}
|
||||
trend={0}
|
||||
accentColor="green"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Success Rate"
|
||||
value={`${stats?.successRate || 0}%`}
|
||||
icon={<PaperPlaneIcon className="size-6" />}
|
||||
trend={0}
|
||||
accentColor="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Automation Workflows */}
|
||||
<ComponentCard title="Automation Workflows" desc="Manage your automated content pipelines">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{automationWorkflows.map((workflow) => {
|
||||
const Icon = workflow.icon;
|
||||
return (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="rounded-2xl border-2 border-slate-200 bg-white p-6 hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className={`inline-flex size-12 rounded-xl bg-gradient-to-br ${workflow.color} items-center justify-center text-white shadow-lg`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
|
||||
workflow.status === "active"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}`}>
|
||||
{workflow.status}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-2">{workflow.name}</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">{workflow.description}</p>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center justify-between text-xs text-slate-600">
|
||||
<span>Schedule:</span>
|
||||
<span className="font-semibold">{workflow.schedule}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-slate-600">
|
||||
<span>Last Run:</span>
|
||||
<span>{workflow.lastRun}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-slate-600">
|
||||
<span>Next Run:</span>
|
||||
<span className="font-semibold text-[var(--color-primary)]">{workflow.nextRun}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between text-xs text-slate-600 mb-1">
|
||||
<span>Coverage</span>
|
||||
<span className="font-semibold">{workflow.coverage}%</span>
|
||||
</div>
|
||||
<ProgressBar value={workflow.coverage} className="h-2" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-slate-100 text-slate-700 px-4 py-2 text-sm font-semibold hover:bg-slate-200 transition">
|
||||
<CloseIcon className="h-4 w-4" />
|
||||
Pause
|
||||
</button>
|
||||
<button className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white px-4 py-2 text-sm font-semibold hover:shadow-lg transition">
|
||||
<PaperPlaneIcon className="h-4 w-4" />
|
||||
Run Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Automation Steps Configuration */}
|
||||
<ComponentCard title="Automation Steps" desc="Configure which steps are automated">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{automationSteps.map((step, index) => {
|
||||
const Icon = step.icon;
|
||||
const isEnabled = stats?.automationCoverage[step.step.toLowerCase() as keyof typeof stats.automationCoverage] || false;
|
||||
return (
|
||||
<Link
|
||||
key={step.step}
|
||||
to={step.path}
|
||||
className="rounded-xl border-2 border-slate-200 bg-white p-5 hover:shadow-lg transition-all group"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className={`inline-flex size-10 rounded-lg bg-gradient-to-br ${
|
||||
isEnabled
|
||||
? "from-[var(--color-success)] to-[var(--color-success-dark)]"
|
||||
: "from-slate-300 to-slate-400"
|
||||
} items-center justify-center text-white shadow-md`}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className={`size-5 rounded-full border-2 flex items-center justify-center ${
|
||||
isEnabled
|
||||
? "border-[var(--color-success)] bg-[var(--color-success)]"
|
||||
: "border-slate-300 bg-white"
|
||||
}`}>
|
||||
{isEnabled && <CheckCircleIcon className="h-3 w-3 text-white" />}
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-1">{step.step}</h4>
|
||||
<p className="text-xs text-slate-600">{step.description}</p>
|
||||
<div className="mt-3 flex items-center gap-1 text-xs text-[var(--color-primary)] opacity-0 group-hover:opacity-100 transition">
|
||||
<span>Configure</span>
|
||||
<ArrowRightIcon className="h-3 w-3" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Activity Chart */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<ComponentCard title="Automation Activity" desc="Last 7 days of automation activity">
|
||||
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
|
||||
<Chart options={chartOptions} series={chartSeries} type="line" height={300} />
|
||||
</Suspense>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<ComponentCard title="Recent Activity" desc="Latest automation executions">
|
||||
<div className="space-y-4">
|
||||
{stats?.recentActivity.map((activity) => (
|
||||
<div
|
||||
key={activity.id}
|
||||
className="flex items-center gap-4 p-4 rounded-lg border border-slate-200 bg-white"
|
||||
>
|
||||
<div className="size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
|
||||
<BoltIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h4 className="font-semibold text-slate-900">{activity.type}</h4>
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
activity.status === "completed"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700"
|
||||
}`}>
|
||||
{activity.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-slate-600">
|
||||
<span>{activity.itemsProcessed} items processed</span>
|
||||
<span>•</span>
|
||||
<span>{new Date(activity.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<ComponentCard title="Quick Actions" desc="Manually trigger automation workflows">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => navigate("/planner/keyword-opportunities")}
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0693e3] hover:shadow-lg transition-all group"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<ListIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-slate-900 mb-1">Run Planner Workflow</h4>
|
||||
<p className="text-sm text-slate-600">Keywords → Clusters → Ideas</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate("/writer/tasks")}
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0bbf87] hover:shadow-lg transition-all group"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<FileTextIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-slate-900 mb-1">Run Writer Workflow</h4>
|
||||
<p className="text-sm text-slate-600">Tasks → Content → Images</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#0bbf87] transition" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate("/writer/published")}
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#5d4ae3] hover:shadow-lg transition-all group"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<PaperPlaneIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-slate-900 mb-1">Run Full Pipeline</h4>
|
||||
<p className="text-sm text-slate-600">Complete end-to-end automation</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#5d4ae3] transition" />
|
||||
</button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import { automationApi, AutomationRule } from '../../api/automation.api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { BoltIcon, PlusIcon, TrashBinIcon, PencilIcon, PaperPlaneIcon, CloseIcon, TaskIcon, ClockIcon } from '../../icons';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
|
||||
export default function AutomationRules() {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector } = useSectorStore();
|
||||
|
||||
const [rules, setRules] = useState<AutomationRule[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedRule, setSelectedRule] = useState<AutomationRule | null>(null);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const loadRules = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await automationApi.listRules({
|
||||
page_size: 100,
|
||||
});
|
||||
setRules(response.results || []);
|
||||
} catch (error: any) {
|
||||
console.error('Error loading rules:', error);
|
||||
toast.error(`Failed to load rules: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
loadRules();
|
||||
}, [loadRules]);
|
||||
|
||||
const handleCreate = () => {
|
||||
setSelectedRule(null);
|
||||
setIsEditMode(false);
|
||||
setIsWizardOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (rule: AutomationRule) => {
|
||||
setSelectedRule(rule);
|
||||
setIsEditMode(true);
|
||||
setIsWizardOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this rule?')) return;
|
||||
|
||||
try {
|
||||
await automationApi.deleteRule(id);
|
||||
toast.success('Rule deleted successfully');
|
||||
loadRules();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to delete rule: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActive = async (rule: AutomationRule) => {
|
||||
try {
|
||||
await automationApi.updateRule(rule.id, {
|
||||
is_active: !rule.is_active,
|
||||
});
|
||||
toast.success(`Rule ${rule.is_active ? 'deactivated' : 'activated'}`);
|
||||
loadRules();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to update rule: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async (id: number) => {
|
||||
try {
|
||||
await automationApi.executeRule(id);
|
||||
toast.success('Rule executed successfully');
|
||||
loadRules();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to execute rule: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (rule: AutomationRule) => {
|
||||
if (!rule.is_active) {
|
||||
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">Inactive</span>;
|
||||
}
|
||||
if (rule.status === 'paused') {
|
||||
return <span className="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300">Paused</span>;
|
||||
}
|
||||
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">Active</span>;
|
||||
};
|
||||
|
||||
const getTriggerBadge = (trigger: string) => {
|
||||
const colors = {
|
||||
schedule: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
||||
event: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300',
|
||||
manual: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300',
|
||||
};
|
||||
return (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${colors[trigger as keyof typeof colors] || colors.manual}`}>
|
||||
{trigger.charAt(0).toUpperCase() + trigger.slice(1)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Automation navigation tabs
|
||||
const automationTabs = [
|
||||
{ label: 'Rules', path: '/automation/rules', icon: <BoltIcon /> },
|
||||
{ label: 'Tasks', path: '/automation/tasks', icon: <ClockIcon /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Automation Rules" />
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Automation Rules"
|
||||
lastUpdated={new Date()}
|
||||
badge={{
|
||||
icon: <BoltIcon />,
|
||||
color: 'purple',
|
||||
}}
|
||||
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Create and manage automation rules to automate your workflows
|
||||
</p>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Loading rules...</div>
|
||||
</div>
|
||||
) : rules.length === 0 ? (
|
||||
<ComponentCard title="No Rules" desc="Create your first automation rule to get started">
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
You haven't created any automation rules yet.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
Create Your First Rule
|
||||
</button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{rules.map((rule) => (
|
||||
<ComponentCard
|
||||
key={rule.id}
|
||||
title={rule.name}
|
||||
desc={rule.description || 'No description'}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{getStatusBadge(rule)}
|
||||
{getTriggerBadge(rule.trigger)}
|
||||
</div>
|
||||
|
||||
{rule.schedule && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>Schedule:</strong> {rule.schedule}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>Actions:</strong> {rule.actions.length}
|
||||
</div>
|
||||
|
||||
{rule.execution_count > 0 && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>Executions:</strong> {rule.execution_count}
|
||||
{rule.last_executed_at && (
|
||||
<span className="ml-2">
|
||||
(Last: {new Date(rule.last_executed_at).toLocaleDateString()})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => handleToggleActive(rule)}
|
||||
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
|
||||
title={rule.is_active ? 'Deactivate' : 'Activate'}
|
||||
>
|
||||
{rule.is_active ? <CloseIcon className="w-4 h-4" /> : <PaperPlaneIcon className="w-4 h-4" />}
|
||||
</button>
|
||||
{rule.trigger === 'manual' && (
|
||||
<button
|
||||
onClick={() => handleExecute(rule.id)}
|
||||
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
|
||||
title="Execute Now"
|
||||
>
|
||||
<PaperPlaneIcon className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleEdit(rule)}
|
||||
className="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center"
|
||||
title="Edit"
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(rule.id)}
|
||||
className="px-3 py-1.5 text-sm rounded-lg border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors flex items-center justify-center"
|
||||
title="Delete"
|
||||
>
|
||||
<TrashBinIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rule Creation/Edit Wizard Modal - TODO: Implement full wizard */}
|
||||
{isWizardOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||
<h3 className="text-xl font-bold mb-4">
|
||||
{isEditMode ? 'Edit Rule' : 'Create Rule'}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Rule wizard coming soon. For now, use the API directly or create rules programmatically.
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setIsWizardOpen(false)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import { automationApi, ScheduledTask } from '../../api/automation.api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { ClockIcon, CheckCircleIcon, XCircleIcon, ArrowRightIcon, BoltIcon } from '../../icons';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
|
||||
export default function AutomationTasks() {
|
||||
const toast = useToast();
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector } = useSectorStore();
|
||||
|
||||
const [tasks, setTasks] = useState<ScheduledTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [ruleFilter, setRuleFilter] = useState<number | null>(null);
|
||||
const [rules, setRules] = useState<Array<{ id: number; name: string }>>([]);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const filters: any = {
|
||||
page_size: 100,
|
||||
ordering: '-scheduled_at',
|
||||
};
|
||||
if (statusFilter !== 'all') {
|
||||
filters.status = statusFilter;
|
||||
}
|
||||
if (ruleFilter) {
|
||||
filters.rule_id = ruleFilter;
|
||||
}
|
||||
const response = await automationApi.listTasks(filters);
|
||||
setTasks(response.results || []);
|
||||
} catch (error: any) {
|
||||
console.error('Error loading tasks:', error);
|
||||
toast.error(`Failed to load tasks: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [statusFilter, ruleFilter, toast]);
|
||||
|
||||
const loadRules = useCallback(async () => {
|
||||
try {
|
||||
const response = await automationApi.listRules({ page_size: 100 });
|
||||
setRules((response.results || []).map(r => ({ id: r.id, name: r.name })));
|
||||
} catch (error: any) {
|
||||
console.error('Error loading rules:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadRules();
|
||||
}, [loadRules]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
const handleRetry = async (id: number) => {
|
||||
try {
|
||||
await automationApi.retryTask(id);
|
||||
toast.success('Task retry initiated');
|
||||
loadTasks();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to retry task: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const badges = {
|
||||
pending: { bg: 'bg-yellow-100', text: 'text-yellow-700', darkBg: 'dark:bg-yellow-900', darkText: 'dark:text-yellow-300', label: 'Pending' },
|
||||
running: { bg: 'bg-blue-100', text: 'text-blue-700', darkBg: 'dark:bg-blue-900', darkText: 'dark:text-blue-300', label: 'Running' },
|
||||
completed: { bg: 'bg-green-100', text: 'text-green-700', darkBg: 'dark:bg-green-900', darkText: 'dark:text-green-300', label: 'Completed' },
|
||||
failed: { bg: 'bg-red-100', text: 'text-red-700', darkBg: 'dark:bg-red-900', darkText: 'dark:text-red-300', label: 'Failed' },
|
||||
cancelled: { bg: 'bg-gray-100', text: 'text-gray-700', darkBg: 'dark:bg-gray-700', darkText: 'dark:text-gray-300', label: 'Cancelled' },
|
||||
};
|
||||
const badge = badges[status as keyof typeof badges] || badges.pending;
|
||||
return (
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${badge.bg} ${badge.text} ${badge.darkBg} ${badge.darkText}`}>
|
||||
{badge.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter(task => {
|
||||
if (statusFilter !== 'all' && task.status !== statusFilter) return false;
|
||||
if (ruleFilter && task.rule_id !== ruleFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Automation navigation tabs
|
||||
const automationTabs = [
|
||||
{ label: 'Rules', path: '/automation/rules', icon: <BoltIcon /> },
|
||||
{ label: 'Tasks', path: '/automation/tasks', icon: <ClockIcon /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Scheduled Tasks" />
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Scheduled Tasks"
|
||||
lastUpdated={new Date()}
|
||||
badge={{
|
||||
icon: <ClockIcon />,
|
||||
color: 'blue',
|
||||
}}
|
||||
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Filter by Status
|
||||
</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800"
|
||||
>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="running">Running</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Filter by Rule
|
||||
</label>
|
||||
<select
|
||||
value={ruleFilter || ''}
|
||||
onChange={(e) => setRuleFilter(e.target.value ? Number(e.target.value) : null)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800"
|
||||
>
|
||||
<option value="">All Rules</option>
|
||||
{rules.map(rule => (
|
||||
<option key={rule.id} value={rule.id}>{rule.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Loading tasks...</div>
|
||||
</div>
|
||||
) : filteredTasks.length === 0 ? (
|
||||
<ComponentCard title="No Tasks" desc="No scheduled tasks found">
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{tasks.length === 0
|
||||
? 'No scheduled tasks have been created yet.'
|
||||
: 'No tasks match the current filters.'}
|
||||
</p>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.map((task) => (
|
||||
<ComponentCard
|
||||
key={task.id}
|
||||
title={`Task #${task.id} - ${task.task_type}`}
|
||||
desc={task.rule_name ? `Rule: ${task.rule_name}` : 'Manual task'}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{getStatusBadge(task.status)}
|
||||
{task.retry_count > 0 && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Retries: {task.retry_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<strong className="text-gray-700 dark:text-gray-300">Scheduled:</strong>
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
{formatDate(task.scheduled_at)}
|
||||
</div>
|
||||
</div>
|
||||
{task.started_at && (
|
||||
<div>
|
||||
<strong className="text-gray-700 dark:text-gray-300">Started:</strong>
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
{formatDate(task.started_at)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{task.completed_at && (
|
||||
<div>
|
||||
<strong className="text-gray-700 dark:text-gray-300">Completed:</strong>
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
{formatDate(task.completed_at)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.error && (
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<XCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong className="text-red-800 dark:text-red-300">Error:</strong>
|
||||
<p className="text-red-700 dark:text-red-400 text-sm mt-1">{task.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.result && task.status === 'completed' && (
|
||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<strong className="text-green-800 dark:text-green-300">Result:</strong>
|
||||
<pre className="text-green-700 dark:text-green-400 text-xs mt-1 overflow-auto">
|
||||
{JSON.stringify(task.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.status === 'failed' && (
|
||||
<div className="flex justify-end pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => handleRetry(task.id)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
Retry Task
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -485,7 +485,7 @@ export default function PlannerDashboard() {
|
||||
<EnhancedMetricCard
|
||||
title="Ideas Generated"
|
||||
value={stats.ideas.total}
|
||||
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} pending`}
|
||||
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} not queued`}
|
||||
icon={<BoltIcon className="size-6" />}
|
||||
accentColor="orange"
|
||||
trend={0}
|
||||
|
||||
Reference in New Issue
Block a user