ai fucntiosn adn otehr atuoamtion fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-14 23:08:48 +00:00
parent cb2d109593
commit 6bb3dd3df4
11 changed files with 1289 additions and 197 deletions

View File

@@ -85,6 +85,47 @@ const AutomationPage: React.FC = () => {
// Track site ID to avoid duplicate calls when activeSite object reference changes
const siteId = activeSite?.id;
/**
* Calculate time remaining until next scheduled run
* Returns formatted string like "in 5h 23m" or "in 2d 3h"
*/
const getNextRunTime = (config: AutomationConfig): string => {
if (!config.is_enabled || !config.scheduled_time) return '';
const now = new Date();
const [schedHours, schedMinutes] = config.scheduled_time.split(':').map(Number);
// Create next run date
const nextRun = new Date();
nextRun.setUTCHours(schedHours, schedMinutes, 0, 0);
// If scheduled time has passed today, set to tomorrow
if (nextRun <= now) {
if (config.frequency === 'daily') {
nextRun.setUTCDate(nextRun.getUTCDate() + 1);
} else if (config.frequency === 'weekly') {
nextRun.setUTCDate(nextRun.getUTCDate() + 7);
}
}
// Calculate difference in milliseconds
const diff = nextRun.getTime() - now.getTime();
const totalMinutes = Math.floor(diff / (1000 * 60));
const totalHours = Math.floor(totalMinutes / 60);
const days = Math.floor(totalHours / 24);
const remainingHours = totalHours % 24;
const remainingMinutes = totalMinutes % 60;
// Format output
if (days > 0) {
return `in ${days}d ${remainingHours}h`;
} else if (remainingHours > 0) {
return `in ${remainingHours}h ${remainingMinutes}m`;
} else {
return `in ${remainingMinutes}m`;
}
};
useEffect(() => {
if (!siteId) return;
// Reset state when site changes
@@ -547,6 +588,14 @@ const AutomationPage: React.FC = () => {
<div className="text-sm text-white/80">
Last: <span className="font-medium">{config.last_run_at ? new Date(config.last_run_at).toLocaleDateString() : 'Never'}</span>
</div>
{config.is_enabled && (
<>
<div className="h-4 w-px bg-white/25"></div>
<div className="text-sm text-white/90">
Next: <span className="font-medium">{getNextRunTime(config)}</span>
</div>
</>
)}
<div className="h-4 w-px bg-white/25"></div>
<div className="text-sm text-white/90">
<span className="font-medium">Est:</span>{' '}

View File

@@ -14,6 +14,9 @@ import {
deleteKeyword,
bulkDeleteKeywords,
bulkUpdateKeywordsStatus,
fetchPlannerKeywordStats,
fetchPlannerKeywordFilterOptions,
FilterOption,
Keyword,
KeywordFilters,
KeywordCreateData,
@@ -53,6 +56,11 @@ export default function Keywords() {
const [totalVolume, setTotalVolume] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Dynamic filter options (loaded from backend based on current data)
const [countryOptions, setCountryOptions] = useState<FilterOption[]>([]);
const [statusOptions, setStatusOptions] = useState<FilterOption[]>([]);
const [clusterOptions, setClusterOptions] = useState<FilterOption[]>([]);
// Filter state - match Keywords.tsx
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
@@ -115,13 +123,32 @@ export default function Keywords() {
loadClusters();
}, []);
// Load dynamic filter options based on current site's data
const loadFilterOptions = useCallback(async () => {
if (!activeSite) return;
try {
const options = await fetchPlannerKeywordFilterOptions(activeSite.id);
setCountryOptions(options.countries || []);
setStatusOptions(options.statuses || []);
setClusterOptions(options.clusters || []);
} catch (error) {
console.error('Error loading filter options:', error);
}
}, [activeSite]);
// Load filter options when site changes
useEffect(() => {
loadFilterOptions();
}, [loadFilterOptions]);
// Load total metrics for footer widget (site-wide totals, no sector filter)
const loadTotalMetrics = useCallback(async () => {
if (!activeSite) return;
try {
// Batch all API calls in parallel for better performance
const [allRes, mappedRes, newRes, imagesRes] = await Promise.all([
const [allRes, mappedRes, newRes, imagesRes, statsRes] = await Promise.all([
// Get total keywords count (site-wide)
fetchKeywords({
page_size: 1,
@@ -141,17 +168,15 @@ export default function Keywords() {
}),
// Get actual total images count
fetchImages({ page_size: 1 }),
// Get total volume from stats endpoint
fetchPlannerKeywordStats(activeSite.id),
]);
setTotalCount(allRes.count || 0);
setTotalClustered(mappedRes.count || 0);
setTotalUnmapped(newRes.count || 0);
setTotalImagesCount(imagesRes.count || 0);
// Get total volume across all keywords (we need to fetch all or rely on backend aggregation)
// For now, we'll just calculate from current data or set to 0
// TODO: Backend should provide total volume as an aggregated metric
setTotalVolume(0);
setTotalVolume(statsRes.total_volume || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
@@ -353,6 +378,14 @@ export default function Keywords() {
const numIds = ids.map(id => parseInt(id));
const sectorId = activeSector?.id;
const selectedKeywords = keywords.filter(k => numIds.includes(k.id));
// Validate single sector - keywords must all be from the same sector
const uniqueSectors = new Set(selectedKeywords.map(k => k.sector_id).filter(Boolean));
if (uniqueSectors.size > 1) {
toast.error(`Selected keywords span ${uniqueSectors.size} different sectors. Please select keywords from a single sector only.`);
return;
}
try {
const result = await autoClusterKeywords(numIds, sectorId);
@@ -516,6 +549,10 @@ export default function Keywords() {
volumeDropdownRef,
setCurrentPage,
loadKeywords,
// Dynamic filter options
countryOptions,
statusOptions,
clusterOptions,
});
}, [
clusters,
@@ -533,6 +570,9 @@ export default function Keywords() {
tempVolumeMax,
loadKeywords,
activeSite,
countryOptions,
statusOptions,
clusterOptions,
]);
// Calculate header metrics - use totalClustered/totalUnmapped from API calls (not page data)