From a8309078c6bbcb3dea764438211e841cb9d89c51 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Thu, 15 Jan 2026 06:19:20 +0000 Subject: [PATCH] filters fixed for all pages of planner writer --- backend/igny8_core/modules/writer/views.py | 85 ++++++++++++--- frontend/src/pages/Planner/Clusters.tsx | 100 ++++++++---------- frontend/src/pages/Planner/Ideas.tsx | 57 ++++------ frontend/src/pages/Writer/Approved.tsx | 53 +++++----- frontend/src/pages/Writer/Content.tsx | 55 +++++----- frontend/src/pages/Writer/Review.tsx | 55 +++++----- frontend/src/pages/Writer/Tasks.tsx | 58 ++++++----- frontend/src/services/api.ts | 115 +++++++++++++++++++-- 8 files changed, 372 insertions(+), 206 deletions(-) diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index 17f56bb1..80e2f6f5 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -988,18 +988,43 @@ class ContentViewSet(SiteSectorModelViewSet): @action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options') def filter_options(self, request): """ - Get distinct filter values from current data. - Returns only values that exist in the current site's content. + Get distinct filter values from current data with cascading support. + Returns only values that exist based on other active filters. """ import logging + from django.db.models import Q logger = logging.getLogger(__name__) try: queryset = self.get_queryset() - # Get distinct statuses - statuses = list(set(queryset.values_list('status', flat=True))) + # Get filter parameters for cascading + status_filter = request.query_params.get('status', '') + site_status_filter = request.query_params.get('site_status', '') + content_type_filter = request.query_params.get('content_type', '') + content_structure_filter = request.query_params.get('content_structure', '') + source_filter = request.query_params.get('source', '') + search = request.query_params.get('search', '') + + # Apply search to base queryset + base_qs = queryset + if search: + base_qs = base_qs.filter( + Q(title__icontains=search) | Q(summary__icontains=search) + ) + + # Get statuses (filtered by other fields) + status_qs = base_qs + if site_status_filter: + status_qs = status_qs.filter(site_status=site_status_filter) + if content_type_filter: + status_qs = status_qs.filter(content_type=content_type_filter) + if content_structure_filter: + status_qs = status_qs.filter(content_structure=content_structure_filter) + if source_filter: + status_qs = status_qs.filter(source=source_filter) + statuses = list(set(status_qs.values_list('status', flat=True))) statuses = sorted([s for s in statuses if s]) status_labels = { 'draft': 'Draft', @@ -1012,8 +1037,17 @@ class ContentViewSet(SiteSectorModelViewSet): for s in statuses ] - # Get distinct site_status - site_statuses = list(set(queryset.values_list('site_status', flat=True))) + # Get site_statuses (filtered by other fields) + site_status_qs = base_qs + if status_filter: + site_status_qs = site_status_qs.filter(status=status_filter) + if content_type_filter: + site_status_qs = site_status_qs.filter(content_type=content_type_filter) + if content_structure_filter: + site_status_qs = site_status_qs.filter(content_structure=content_structure_filter) + if source_filter: + site_status_qs = site_status_qs.filter(source=source_filter) + site_statuses = list(set(site_status_qs.values_list('site_status', flat=True))) site_statuses = sorted([s for s in site_statuses if s]) site_status_labels = { 'not_published': 'Not Published', @@ -1027,8 +1061,17 @@ class ContentViewSet(SiteSectorModelViewSet): for s in site_statuses ] - # Get distinct content_types - content_types = list(set(queryset.values_list('content_type', flat=True))) + # Get content types (filtered by other fields) + type_qs = base_qs + if status_filter: + type_qs = type_qs.filter(status=status_filter) + if site_status_filter: + type_qs = type_qs.filter(site_status=site_status_filter) + if content_structure_filter: + type_qs = type_qs.filter(content_structure=content_structure_filter) + if source_filter: + type_qs = type_qs.filter(source=source_filter) + content_types = list(set(type_qs.values_list('content_type', flat=True))) content_types = sorted([t for t in content_types if t]) type_labels = { 'post': 'Post', @@ -1041,8 +1084,17 @@ class ContentViewSet(SiteSectorModelViewSet): for t in content_types ] - # Get distinct content_structures - structures = list(set(queryset.values_list('content_structure', flat=True))) + # Get content structures (filtered by other fields) + structure_qs = base_qs + if status_filter: + structure_qs = structure_qs.filter(status=status_filter) + if site_status_filter: + structure_qs = structure_qs.filter(site_status=site_status_filter) + if content_type_filter: + structure_qs = structure_qs.filter(content_type=content_type_filter) + if source_filter: + structure_qs = structure_qs.filter(source=source_filter) + structures = list(set(structure_qs.values_list('content_structure', flat=True))) structures = sorted([s for s in structures if s]) structure_labels = { 'article': 'Article', 'guide': 'Guide', 'comparison': 'Comparison', @@ -1057,8 +1109,17 @@ class ContentViewSet(SiteSectorModelViewSet): for s in structures ] - # Get distinct sources - sources = list(set(queryset.values_list('source', flat=True))) + # Get sources (filtered by other fields) + source_qs = base_qs + if status_filter: + source_qs = source_qs.filter(status=status_filter) + if site_status_filter: + source_qs = source_qs.filter(site_status=site_status_filter) + if content_type_filter: + source_qs = source_qs.filter(content_type=content_type_filter) + if content_structure_filter: + source_qs = source_qs.filter(content_structure=content_structure_filter) + sources = list(set(source_qs.values_list('source', flat=True))) sources = sorted([s for s in sources if s]) source_labels = { 'igny8': 'IGNY8', diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index a71623d6..1fe1e5bc 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -15,6 +15,7 @@ import { bulkDeleteClusters, bulkUpdateClustersStatus, autoGenerateIdeas, + fetchPlannerClusterFilterOptions, Cluster, ClusterFilters, ClusterCreateData, @@ -91,73 +92,62 @@ export default function Clusters() { const progressModal = useProgressModal(); const hasReloadedRef = useRef(false); - // Load dynamic filter options based on current site's data - const loadFilterOptions = useCallback(async () => { + // Parse difficulty filter to min/max values + // Backend uses: 1=0-10, 2=11-30, 3=31-50, 4=51-70, 5=71-100 + const getDifficultyRange = useCallback((filter: string): { min?: number; max?: number } => { + if (!filter) return {}; + const level = parseInt(filter, 10); + if (isNaN(level)) return {}; + // Map difficulty level to raw difficulty range matching backend logic + const ranges: Record = { + 1: { min: 0, max: 10 }, + 2: { min: 11, max: 30 }, + 3: { min: 31, max: 50 }, + 4: { min: 51, max: 70 }, + 5: { min: 71, max: 100 }, + }; + return ranges[level] || {}; + }, []); + + // Load dynamic filter options based on current site's data and applied filters + // This implements cascading filters - each filter's options reflect what's available + // given the other currently applied filters + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + difficulty_min?: number; + difficulty_max?: number; + volume_min?: number; + volume_max?: number; + search?: string; + }) => { if (!activeSite) return; try { - const response = await fetch(`/api/v1/planner/clusters/filter_options/?site_id=${activeSite.id}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setDifficultyOptions(result.data.difficulties || []); - } + const options = await fetchPlannerClusterFilterOptions(activeSite.id, currentFilters); + setStatusOptions(options.statuses || []); + setDifficultyOptions(options.difficulties || []); } catch (error) { console.error('Error loading filter options:', error); } }, [activeSite]); - // Load filter options when site changes + // Load filter options when site changes (initial load with no filters) useEffect(() => { loadFilterOptions(); - }, [loadFilterOptions]); + }, [activeSite]); - // Reload filter options when filters change (cascading) + // Reload filter options when any filter changes (cascading filters) useEffect(() => { - if (!activeSite) return; - - const loadCascadingOptions = async () => { - try { - const params = new URLSearchParams(); - params.append('site_id', activeSite.id.toString()); - if (statusFilter) params.append('status', statusFilter); - if (difficultyFilter) { - // Map difficulty level to min/max range for backend - const difficultyNum = parseInt(difficultyFilter); - const ranges: Record = { - 1: { min: 0, max: 10 }, - 2: { min: 11, max: 30 }, - 3: { min: 31, max: 50 }, - 4: { min: 51, max: 70 }, - 5: { min: 71, max: 100 }, - }; - const range = ranges[difficultyNum]; - if (range) { - params.append('difficulty_min', range.min.toString()); - params.append('difficulty_max', range.max.toString()); - } - } - if (volumeMin) params.append('volume_min', volumeMin.toString()); - if (volumeMax) params.append('volume_max', volumeMax.toString()); - if (searchTerm) params.append('search', searchTerm); - - const response = await fetch(`/api/v1/planner/clusters/filter_options/?${params.toString()}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setDifficultyOptions(result.data.difficulties || []); - } - } catch (error) { - console.error('Error loading cascading filter options:', error); - } - }; - - loadCascadingOptions(); - }, [activeSite, statusFilter, difficultyFilter, volumeMin, volumeMax, searchTerm]); + const { min: difficultyMin, max: difficultyMax } = getDifficultyRange(difficultyFilter); + loadFilterOptions({ + status: statusFilter || undefined, + difficulty_min: difficultyMin, + difficulty_max: difficultyMax, + volume_min: volumeMin !== '' ? Number(volumeMin) : undefined, + volume_max: volumeMax !== '' ? Number(volumeMax) : undefined, + search: searchTerm || undefined, + }); + }, [statusFilter, difficultyFilter, volumeMin, volumeMax, searchTerm, loadFilterOptions, getDifficultyRange]); // Load total metrics for footer widget (site-wide totals, no sector filter) const loadTotalMetrics = useCallback(async () => { diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index 45f10d80..799d7c5f 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -107,12 +107,20 @@ export default function Ideas() { loadClusters(); }, []); - // Load dynamic filter options based on current site's data - const loadFilterOptions = useCallback(async () => { + // Load dynamic filter options based on current site's data and applied filters + // This implements cascading filters - each filter's options reflect what's available + // given the other currently applied filters + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + content_type?: string; + content_structure?: string; + cluster?: string; + search?: string; + }) => { if (!activeSite) return; try { - const options = await fetchPlannerIdeasFilterOptions(activeSite.id); + const options = await fetchPlannerIdeasFilterOptions(activeSite.id, currentFilters); setStatusOptions(options.statuses || []); setContentTypeOptions(options.content_types || []); setContentStructureOptions(options.content_structures || []); @@ -122,42 +130,21 @@ export default function Ideas() { } }, [activeSite]); - // Load filter options when site changes + // Load filter options when site changes (initial load with no filters) useEffect(() => { loadFilterOptions(); - }, [loadFilterOptions]); + }, [activeSite]); - // Reload filter options when filters change (cascading) + // Reload filter options when any filter changes (cascading filters) useEffect(() => { - if (!activeSite) return; - - const loadCascadingOptions = async () => { - try { - const params = new URLSearchParams(); - params.append('site_id', activeSite.id.toString()); - if (statusFilter) params.append('status', statusFilter); - if (typeFilter) params.append('content_type', typeFilter); - if (structureFilter) params.append('content_structure', structureFilter); - if (clusterFilter) params.append('cluster', clusterFilter); - if (searchTerm) params.append('search', searchTerm); - - const response = await fetch(`/api/v1/planner/ideas/filter_options/?${params.toString()}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setContentTypeOptions(result.data.content_types || []); - setContentStructureOptions(result.data.content_structures || []); - setClusterOptions(result.data.clusters || []); - } - } catch (error) { - console.error('Error loading cascading filter options:', error); - } - }; - - loadCascadingOptions(); - }, [activeSite, statusFilter, typeFilter, structureFilter, clusterFilter, searchTerm]); + loadFilterOptions({ + status: statusFilter || undefined, + content_type: typeFilter || undefined, + content_structure: structureFilter || undefined, + cluster: clusterFilter || undefined, + search: searchTerm || undefined, + }); + }, [statusFilter, typeFilter, structureFilter, clusterFilter, searchTerm, loadFilterOptions]); // Load total metrics for footer widget (site-wide totals, no sector filter) const loadTotalMetrics = useCallback(async () => { diff --git a/frontend/src/pages/Writer/Approved.tsx b/frontend/src/pages/Writer/Approved.tsx index 2605ab0d..c82c9d09 100644 --- a/frontend/src/pages/Writer/Approved.tsx +++ b/frontend/src/pages/Writer/Approved.tsx @@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, fetchImages, + fetchWriterContentFilterOptions, Content, ContentListResponse, ContentFilters, @@ -69,38 +70,44 @@ export default function Approved() { const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [showContent, setShowContent] = useState(false); - // Load dynamic filter options with cascading - const loadFilterOptions = useCallback(async () => { + // Load dynamic filter options based on current site's data and applied filters + // This implements cascading filters - each filter's options reflect what's available + // given the other currently applied filters + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + site_status?: string; + content_type?: string; + content_structure?: string; + search?: string; + }) => { if (!activeSite) return; try { - const params = new URLSearchParams(); - params.append('site_id', activeSite.id.toString()); - if (statusFilter) params.append('status', statusFilter); - if (siteStatusFilter) params.append('site_status', siteStatusFilter); - if (contentTypeFilter) params.append('content_type', contentTypeFilter); - if (contentStructureFilter) params.append('content_structure', contentStructureFilter); - if (searchTerm) params.append('search', searchTerm); - - const response = await fetch(`/api/v1/writer/content/filter_options/?${params.toString()}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setSiteStatusOptions(result.data.site_statuses || []); - setContentTypeOptions(result.data.content_types || []); - setContentStructureOptions(result.data.content_structures || []); - } + const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); + setStatusOptions(options.statuses || []); + setSiteStatusOptions(options.site_statuses || []); + setContentTypeOptions(options.content_types || []); + setContentStructureOptions(options.content_structures || []); } catch (error) { console.error('Error loading filter options:', error); } - }, [activeSite, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm]); + }, [activeSite]); - // Load filter options when dependencies change + // Load filter options when site changes (initial load with no filters) useEffect(() => { loadFilterOptions(); - }, [loadFilterOptions]); + }, [activeSite]); + + // Reload filter options when any filter changes (cascading filters) + useEffect(() => { + loadFilterOptions({ + status: statusFilter || undefined, + site_status: siteStatusFilter || undefined, + content_type: contentTypeFilter || undefined, + content_structure: contentStructureFilter || undefined, + search: searchTerm || undefined, + }); + }, [statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm, loadFilterOptions]); // Load total metrics for footer widget and header metrics (not affected by pagination) const loadTotalMetrics = useCallback(async () => { diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index c7a74ada..3123fa87 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, fetchImages, + fetchWriterContentFilterOptions, Content as ContentType, ContentFilters, generateImagePrompts, @@ -74,38 +75,46 @@ export default function Content() { const progressModal = useProgressModal(); const hasReloadedRef = useRef(false); - // Load dynamic filter options - const loadFilterOptions = useCallback(async () => { + // Load dynamic filter options based on current site's data and applied filters + // This implements cascading filters - each filter's options reflect what's available + // given the other currently applied filters + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + site_status?: string; + content_type?: string; + content_structure?: string; + source?: string; + search?: string; + }) => { if (!activeSite) return; try { - const params = new URLSearchParams(); - params.append('site_id', activeSite.id.toString()); - if (statusFilter) params.append('status', statusFilter); - if (sourceFilter) params.append('source', sourceFilter); - if (contentTypeFilter) params.append('content_type', contentTypeFilter); - if (contentStructureFilter) params.append('content_structure', contentStructureFilter); - if (searchTerm) params.append('search', searchTerm); - - const response = await fetch(`/api/v1/writer/content/filter_options/?${params.toString()}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setSourceOptions(result.data.sources || []); - setContentTypeOptions(result.data.content_types || []); - setContentStructureOptions(result.data.content_structures || []); - } + const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); + setStatusOptions(options.statuses || []); + setSourceOptions(options.sources || []); + setContentTypeOptions(options.content_types || []); + setContentStructureOptions(options.content_structures || []); } catch (error) { console.error('Error loading filter options:', error); } - }, [activeSite, statusFilter, sourceFilter, contentTypeFilter, contentStructureFilter, searchTerm]); + }, [activeSite]); - // Load filter options when dependencies change + // Load filter options when site changes (initial load with no filters) useEffect(() => { loadFilterOptions(); - }, [loadFilterOptions]); + }, [activeSite]); + + // Reload filter options when any filter changes (cascading filters) + useEffect(() => { + loadFilterOptions({ + status: statusFilter || undefined, + content_type: contentTypeFilter || undefined, + content_structure: contentStructureFilter || undefined, + source: sourceFilter || undefined, + search: searchTerm || undefined, + }); + }, [statusFilter, contentTypeFilter, contentStructureFilter, sourceFilter, searchTerm, loadFilterOptions]); + // Load total metrics for footer widget and header metrics (site-wide totals, no sector filter) const loadTotalMetrics = useCallback(async () => { diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index 765811fc..e2c37416 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, fetchImages, + fetchWriterContentFilterOptions, Content, ContentListResponse, ContentFilters, @@ -67,38 +68,44 @@ export default function Review() { const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [showContent, setShowContent] = useState(false); - // Load dynamic filter options - const loadFilterOptions = useCallback(async () => { + // Load dynamic filter options based on current site's data and applied filters + // This implements cascading filters - each filter's options reflect what's available + // given the other currently applied filters + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + site_status?: string; + content_type?: string; + content_structure?: string; + search?: string; + }) => { if (!activeSite) return; try { - const params = new URLSearchParams(); - params.append('site_id', activeSite.id.toString()); - params.append('status', 'review'); // Always review status - if (siteStatusFilter) params.append('site_status', siteStatusFilter); - if (contentTypeFilter) params.append('content_type', contentTypeFilter); - if (contentStructureFilter) params.append('content_structure', contentStructureFilter); - if (searchTerm) params.append('search', searchTerm); - - const response = await fetch(`/api/v1/writer/content/filter_options/?${params.toString()}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setSiteStatusOptions(result.data.site_statuses || []); - setContentTypeOptions(result.data.content_types || []); - setContentStructureOptions(result.data.content_structures || []); - } + const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); + setStatusOptions(options.statuses || []); + setSiteStatusOptions(options.site_statuses || []); + setContentTypeOptions(options.content_types || []); + setContentStructureOptions(options.content_structures || []); } catch (error) { console.error('Error loading filter options:', error); } - }, [activeSite, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm]); + }, [activeSite]); - // Load filter options when dependencies change + // Load filter options when site changes (initial load with no filters) useEffect(() => { - loadFilterOptions(); - }, [loadFilterOptions]); + loadFilterOptions({ status: 'review' }); // Always pass review status + }, [activeSite]); + + // Reload filter options when any filter changes (cascading filters) + useEffect(() => { + loadFilterOptions({ + status: 'review', // Always review status + site_status: siteStatusFilter || undefined, + content_type: contentTypeFilter || undefined, + content_structure: contentStructureFilter || undefined, + search: searchTerm || undefined, + }); + }, [siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm, loadFilterOptions]); // Load content - filtered for review status const loadContent = useCallback(async () => { diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 1b883e8f..c7dc017b 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -17,6 +17,7 @@ import { bulkUpdateTasksStatus, autoGenerateContent, autoGenerateImages, + fetchWriterTaskFilterOptions, Task, TasksFilters, TaskCreateData, @@ -106,40 +107,47 @@ export default function Tasks() { const hasReloadedRef = useRef(false); - // Load dynamic filter options with cascading - const loadFilterOptions = useCallback(async () => { + // Load dynamic filter options based on current site's data and applied filters + // This implements cascading filters - each filter's options reflect what's available + // given the other currently applied filters + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + content_type?: string; + content_structure?: string; + cluster?: string; + source?: string; + search?: string; + }) => { if (!activeSite) return; try { - const params = new URLSearchParams(); - params.append('site_id', activeSite.id.toString()); - if (statusFilter) params.append('status', statusFilter); - if (typeFilter) params.append('content_type', typeFilter); - if (structureFilter) params.append('content_structure', structureFilter); - if (clusterFilter) params.append('cluster', clusterFilter); - if (sourceFilter) params.append('source', sourceFilter); - if (searchTerm) params.append('search', searchTerm); - - const response = await fetch(`/api/v1/writer/tasks/filter_options/?${params.toString()}`, { - credentials: 'include', - }); - const result = await response.json(); - if (result.success && result.data) { - setStatusOptions(result.data.statuses || []); - setContentTypeOptions(result.data.content_types || []); - setContentStructureOptions(result.data.content_structures || []); - setClusterOptions(result.data.clusters || []); - setSourceOptions(result.data.sources || []); - } + const options = await fetchWriterTaskFilterOptions(activeSite.id, currentFilters); + setStatusOptions(options.statuses || []); + setContentTypeOptions(options.content_types || []); + setContentStructureOptions(options.content_structures || []); + setClusterOptions(options.clusters || []); + setSourceOptions(options.sources || []); } catch (error) { console.error('Error loading filter options:', error); } - }, [activeSite, statusFilter, typeFilter, structureFilter, clusterFilter, sourceFilter, searchTerm]); + }, [activeSite]); - // Load filter options when dependencies change + // Load filter options when site changes (initial load with no filters) useEffect(() => { loadFilterOptions(); - }, [loadFilterOptions]); + }, [activeSite]); + + // Reload filter options when any filter changes (cascading filters) + useEffect(() => { + loadFilterOptions({ + status: statusFilter || undefined, + content_type: typeFilter || undefined, + content_structure: structureFilter || undefined, + cluster: clusterFilter || undefined, + source: sourceFilter || undefined, + search: searchTerm || undefined, + }); + }, [statusFilter, typeFilter, structureFilter, clusterFilter, sourceFilter, searchTerm, loadFilterOptions]); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 5b3810f7..9acccd66 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -789,11 +789,33 @@ export async function fetchPlannerKeywordFilterOptions( // Clusters filter options export interface PlannerClusterFilterOptions { statuses: FilterOption[]; + difficulties: FilterOption[]; } -export async function fetchPlannerClusterFilterOptions(siteId?: number): Promise { - const queryParams = siteId ? `?site_id=${siteId}` : ''; - return fetchAPI(`/v1/planner/clusters/filter_options/${queryParams}`); +export interface ClusterFilterOptionsRequest { + status?: string; + difficulty_min?: number; + difficulty_max?: number; + volume_min?: number; + volume_max?: number; + search?: string; +} + +export async function fetchPlannerClusterFilterOptions( + siteId?: number, + filters?: ClusterFilterOptionsRequest +): Promise { + const params = new URLSearchParams(); + if (siteId) params.append('site_id', siteId.toString()); + if (filters?.status) params.append('status', filters.status); + if (filters?.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString()); + if (filters?.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString()); + if (filters?.volume_min !== undefined) params.append('volume_min', filters.volume_min.toString()); + if (filters?.volume_max !== undefined) params.append('volume_max', filters.volume_max.toString()); + if (filters?.search) params.append('search', filters.search); + + const queryString = params.toString(); + return fetchAPI(`/v1/planner/clusters/filter_options/${queryString ? `?${queryString}` : ''}`); } // Ideas filter options @@ -804,9 +826,63 @@ export interface PlannerIdeasFilterOptions { clusters: FilterOption[]; } -export async function fetchPlannerIdeasFilterOptions(siteId?: number): Promise { - const queryParams = siteId ? `?site_id=${siteId}` : ''; - return fetchAPI(`/v1/planner/ideas/filter_options/${queryParams}`); +export interface IdeasFilterOptionsRequest { + status?: string; + content_type?: string; + content_structure?: string; + cluster?: string; + search?: string; +} + +export async function fetchPlannerIdeasFilterOptions( + siteId?: number, + filters?: IdeasFilterOptionsRequest +): Promise { + const params = new URLSearchParams(); + if (siteId) params.append('site_id', siteId.toString()); + if (filters?.status) params.append('status', filters.status); + if (filters?.content_type) params.append('content_type', filters.content_type); + if (filters?.content_structure) params.append('content_structure', filters.content_structure); + if (filters?.cluster) params.append('cluster', filters.cluster); + if (filters?.search) params.append('search', filters.search); + + const queryString = params.toString(); + return fetchAPI(`/v1/planner/ideas/filter_options/${queryString ? `?${queryString}` : ''}`); +} + +// Tasks filter options (Writer module) +export interface WriterTaskFilterOptions { + statuses: FilterOption[]; + content_types: FilterOption[]; + content_structures: FilterOption[]; + clusters: FilterOption[]; + sources: FilterOption[]; +} + +export interface TaskFilterOptionsRequest { + status?: string; + content_type?: string; + content_structure?: string; + cluster?: string; + source?: string; + search?: string; +} + +export async function fetchWriterTaskFilterOptions( + siteId?: number, + filters?: TaskFilterOptionsRequest +): Promise { + const params = new URLSearchParams(); + if (siteId) params.append('site_id', siteId.toString()); + if (filters?.status) params.append('status', filters.status); + if (filters?.content_type) params.append('content_type', filters.content_type); + if (filters?.content_structure) params.append('content_structure', filters.content_structure); + if (filters?.cluster) params.append('cluster', filters.cluster); + if (filters?.source) params.append('source', filters.source); + if (filters?.search) params.append('search', filters.search); + + const queryString = params.toString(); + return fetchAPI(`/v1/writer/tasks/filter_options/${queryString ? `?${queryString}` : ''}`); } // Content filter options (Writer module) @@ -818,9 +894,30 @@ export interface WriterContentFilterOptions { sources: FilterOption[]; } -export async function fetchWriterContentFilterOptions(siteId?: number): Promise { - const queryParams = siteId ? `?site_id=${siteId}` : ''; - return fetchAPI(`/v1/writer/content/filter_options/${queryParams}`); +export interface ContentFilterOptionsRequest { + status?: string; + site_status?: string; + content_type?: string; + content_structure?: string; + source?: string; + search?: string; +} + +export async function fetchWriterContentFilterOptions( + siteId?: number, + filters?: ContentFilterOptionsRequest +): Promise { + const params = new URLSearchParams(); + if (siteId) params.append('site_id', siteId.toString()); + if (filters?.status) params.append('status', filters.status); + if (filters?.site_status) params.append('site_status', filters.site_status); + if (filters?.content_type) params.append('content_type', filters.content_type); + if (filters?.content_structure) params.append('content_structure', filters.content_structure); + if (filters?.source) params.append('source', filters.source); + if (filters?.search) params.append('search', filters.search); + + const queryString = params.toString(); + return fetchAPI(`/v1/writer/content/filter_options/${queryString ? `?${queryString}` : ''}`); } // Clusters-specific API functions