Est:{' '}
diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx
index ea8aa73f..cf655a82 100644
--- a/frontend/src/pages/Planner/Keywords.tsx
+++ b/frontend/src/pages/Planner/Keywords.tsx
@@ -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
([]);
+ const [statusOptions, setStatusOptions] = useState([]);
+ const [clusterOptions, setClusterOptions] = useState([]);
+
// 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)
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index f4d899f7..53ec0474 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -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 {
+ 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 {
+ const queryParams = siteId ? `?site_id=${siteId}` : '';
+ return fetchAPI(`/v1/planner/keywords/filter_options/${queryParams}`);
+}
+
// Clusters-specific API functions
export interface ClusterFilters {
search?: string;