ai fucntiosn adn otehr atuoamtion fixes
This commit is contained in:
@@ -136,6 +136,10 @@ export const createKeywordsPageConfig = (
|
||||
volumeDropdownRef: React.RefObject<HTMLDivElement | null>;
|
||||
setCurrentPage: (page: number) => void;
|
||||
loadKeywords: () => Promise<void>;
|
||||
// Dynamic filter options
|
||||
countryOptions?: Array<{ value: string; label: string }>;
|
||||
statusOptions?: Array<{ value: string; label: string }>;
|
||||
clusterOptions?: Array<{ value: string; label: string }>;
|
||||
}
|
||||
): KeywordsPageConfig => {
|
||||
const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors
|
||||
@@ -267,8 +271,14 @@ export const createKeywordsPageConfig = (
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Status' },
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'mapped', label: 'Mapped' },
|
||||
// Use dynamic options if available, otherwise show default options
|
||||
...(handlers.statusOptions && handlers.statusOptions.length > 0
|
||||
? handlers.statusOptions
|
||||
: [
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'mapped', label: 'Mapped' },
|
||||
]
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -277,13 +287,19 @@ export const createKeywordsPageConfig = (
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '', label: 'All Countries' },
|
||||
{ value: 'US', label: 'United States' },
|
||||
{ value: 'CA', label: 'Canada' },
|
||||
{ value: 'GB', label: 'United Kingdom' },
|
||||
{ value: 'AE', label: 'United Arab Emirates' },
|
||||
{ value: 'AU', label: 'Australia' },
|
||||
{ value: 'IN', label: 'India' },
|
||||
{ value: 'PK', label: 'Pakistan' },
|
||||
// Use dynamic options if available, otherwise show default options
|
||||
...(handlers.countryOptions && handlers.countryOptions.length > 0
|
||||
? handlers.countryOptions
|
||||
: [
|
||||
{ value: 'US', label: 'United States' },
|
||||
{ value: 'CA', label: 'Canada' },
|
||||
{ value: 'GB', label: 'United Kingdom' },
|
||||
{ value: 'AE', label: 'United Arab Emirates' },
|
||||
{ value: 'AU', label: 'Australia' },
|
||||
{ value: 'IN', label: 'India' },
|
||||
{ value: 'PK', label: 'Pakistan' },
|
||||
]
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -299,6 +315,20 @@ export const createKeywordsPageConfig = (
|
||||
{ value: '5', label: '5 - Very Hard' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'cluster',
|
||||
label: 'Cluster',
|
||||
type: 'select',
|
||||
dynamicOptions: 'clusters', // Flag for dynamic option loading
|
||||
options: [
|
||||
{ value: '', label: 'All Clusters' },
|
||||
// Use dynamic cluster options if available
|
||||
...(handlers.clusterOptions && handlers.clusterOptions.length > 0
|
||||
? handlers.clusterOptions
|
||||
: handlers.clusters.map(c => ({ value: String(c.id), label: c.name }))
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'volume',
|
||||
label: 'Volume Range',
|
||||
|
||||
@@ -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>{' '}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -730,6 +730,34 @@ export async function bulkUpdateKeywordsStatus(ids: number[], status: string): P
|
||||
});
|
||||
}
|
||||
|
||||
// Planner keyword stats interface and function
|
||||
export interface PlannerKeywordStats {
|
||||
total_keywords: number;
|
||||
total_volume: number;
|
||||
}
|
||||
|
||||
export async function fetchPlannerKeywordStats(siteId?: number): Promise<PlannerKeywordStats> {
|
||||
const queryParams = siteId ? `?site_id=${siteId}` : '';
|
||||
return fetchAPI(`/v1/planner/keywords/stats/${queryParams}`);
|
||||
}
|
||||
|
||||
// Planner keyword filter options interface and function
|
||||
export interface FilterOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface PlannerKeywordFilterOptions {
|
||||
countries: FilterOption[];
|
||||
statuses: FilterOption[];
|
||||
clusters: FilterOption[];
|
||||
}
|
||||
|
||||
export async function fetchPlannerKeywordFilterOptions(siteId?: number): Promise<PlannerKeywordFilterOptions> {
|
||||
const queryParams = siteId ? `?site_id=${siteId}` : '';
|
||||
return fetchAPI(`/v1/planner/keywords/filter_options/${queryParams}`);
|
||||
}
|
||||
|
||||
// Clusters-specific API functions
|
||||
export interface ClusterFilters {
|
||||
search?: string;
|
||||
|
||||
Reference in New Issue
Block a user