fitlers fixes
This commit is contained in:
@@ -776,9 +776,12 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
cluster_filter = request.query_params.get('cluster_id')
|
cluster_filter = request.query_params.get('cluster_id')
|
||||||
difficulty_min = request.query_params.get('difficulty_min')
|
difficulty_min = request.query_params.get('difficulty_min')
|
||||||
difficulty_max = request.query_params.get('difficulty_max')
|
difficulty_max = request.query_params.get('difficulty_max')
|
||||||
|
volume_min = request.query_params.get('volume_min')
|
||||||
|
volume_max = request.query_params.get('volume_max')
|
||||||
|
search_term = request.query_params.get('search')
|
||||||
|
|
||||||
# Base queryset for each filter option calculation
|
# Base queryset for each filter option calculation
|
||||||
# For countries: apply status, cluster, difficulty filters
|
# For countries: apply status, cluster, difficulty, volume, search filters
|
||||||
countries_qs = queryset
|
countries_qs = queryset
|
||||||
if status_filter:
|
if status_filter:
|
||||||
countries_qs = countries_qs.filter(status=status_filter)
|
countries_qs = countries_qs.filter(status=status_filter)
|
||||||
@@ -800,8 +803,23 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
if volume_min is not None:
|
||||||
|
try:
|
||||||
|
countries_qs = countries_qs.filter(seed_keyword__volume__gte=int(volume_min))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if volume_max is not None:
|
||||||
|
try:
|
||||||
|
countries_qs = countries_qs.filter(seed_keyword__volume__lte=int(volume_max))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if search_term:
|
||||||
|
countries_qs = countries_qs.filter(
|
||||||
|
Q(seed_keyword__keyword__icontains=search_term) |
|
||||||
|
Q(cluster__name__icontains=search_term)
|
||||||
|
)
|
||||||
|
|
||||||
# For statuses: apply country, cluster, difficulty filters
|
# For statuses: apply country, cluster, difficulty, volume, search filters
|
||||||
statuses_qs = queryset
|
statuses_qs = queryset
|
||||||
if country_filter:
|
if country_filter:
|
||||||
statuses_qs = statuses_qs.filter(seed_keyword__country=country_filter)
|
statuses_qs = statuses_qs.filter(seed_keyword__country=country_filter)
|
||||||
@@ -823,8 +841,23 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
if volume_min is not None:
|
||||||
|
try:
|
||||||
|
statuses_qs = statuses_qs.filter(seed_keyword__volume__gte=int(volume_min))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if volume_max is not None:
|
||||||
|
try:
|
||||||
|
statuses_qs = statuses_qs.filter(seed_keyword__volume__lte=int(volume_max))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if search_term:
|
||||||
|
statuses_qs = statuses_qs.filter(
|
||||||
|
Q(seed_keyword__keyword__icontains=search_term) |
|
||||||
|
Q(cluster__name__icontains=search_term)
|
||||||
|
)
|
||||||
|
|
||||||
# For clusters: apply status, country, difficulty filters
|
# For clusters: apply status, country, difficulty, volume, search filters
|
||||||
clusters_qs = queryset
|
clusters_qs = queryset
|
||||||
if status_filter:
|
if status_filter:
|
||||||
clusters_qs = clusters_qs.filter(status=status_filter)
|
clusters_qs = clusters_qs.filter(status=status_filter)
|
||||||
@@ -846,8 +879,23 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
if volume_min is not None:
|
||||||
|
try:
|
||||||
|
clusters_qs = clusters_qs.filter(seed_keyword__volume__gte=int(volume_min))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if volume_max is not None:
|
||||||
|
try:
|
||||||
|
clusters_qs = clusters_qs.filter(seed_keyword__volume__lte=int(volume_max))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if search_term:
|
||||||
|
clusters_qs = clusters_qs.filter(
|
||||||
|
Q(seed_keyword__keyword__icontains=search_term) |
|
||||||
|
Q(cluster__name__icontains=search_term)
|
||||||
|
)
|
||||||
|
|
||||||
# For difficulties: apply status, country, cluster filters
|
# For difficulties: apply status, country, cluster, volume, search filters
|
||||||
difficulties_qs = queryset
|
difficulties_qs = queryset
|
||||||
if status_filter:
|
if status_filter:
|
||||||
difficulties_qs = difficulties_qs.filter(status=status_filter)
|
difficulties_qs = difficulties_qs.filter(status=status_filter)
|
||||||
@@ -855,6 +903,21 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
difficulties_qs = difficulties_qs.filter(seed_keyword__country=country_filter)
|
difficulties_qs = difficulties_qs.filter(seed_keyword__country=country_filter)
|
||||||
if cluster_filter:
|
if cluster_filter:
|
||||||
difficulties_qs = difficulties_qs.filter(cluster_id=cluster_filter)
|
difficulties_qs = difficulties_qs.filter(cluster_id=cluster_filter)
|
||||||
|
if volume_min is not None:
|
||||||
|
try:
|
||||||
|
difficulties_qs = difficulties_qs.filter(seed_keyword__volume__gte=int(volume_min))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if volume_max is not None:
|
||||||
|
try:
|
||||||
|
difficulties_qs = difficulties_qs.filter(seed_keyword__volume__lte=int(volume_max))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if search_term:
|
||||||
|
difficulties_qs = difficulties_qs.filter(
|
||||||
|
Q(seed_keyword__keyword__icontains=search_term) |
|
||||||
|
Q(cluster__name__icontains=search_term)
|
||||||
|
)
|
||||||
|
|
||||||
# Get distinct countries
|
# Get distinct countries
|
||||||
countries = list(set(countries_qs.values_list('seed_keyword__country', flat=True)))
|
countries = list(set(countries_qs.values_list('seed_keyword__country', flat=True)))
|
||||||
@@ -1259,18 +1322,56 @@ class ClusterViewSet(SiteSectorModelViewSet):
|
|||||||
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
|
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
|
||||||
def filter_options(self, request):
|
def filter_options(self, request):
|
||||||
"""
|
"""
|
||||||
Get distinct filter values from current data.
|
Get distinct filter values from current data with cascading filter support.
|
||||||
Returns only statuses that exist in the current site's clusters.
|
Returns only statuses and difficulties that exist based on other active filters.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from django.db.models import Q, Sum, Avg, Case, When, F, IntegerField
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Start with base queryset (already has volume/difficulty annotations from get_queryset)
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
# Get filter parameters for cascading
|
||||||
|
status_filter = request.query_params.get('status', '')
|
||||||
|
difficulty_min = request.query_params.get('difficulty_min', '')
|
||||||
|
difficulty_max = request.query_params.get('difficulty_max', '')
|
||||||
|
volume_min = request.query_params.get('volume_min', '')
|
||||||
|
volume_max = request.query_params.get('volume_max', '')
|
||||||
|
search = request.query_params.get('search', '')
|
||||||
|
|
||||||
|
# ===== GET STATUS OPTIONS =====
|
||||||
|
# Apply OTHER filters (exclude status) to get valid status options
|
||||||
|
status_qs = queryset
|
||||||
|
if difficulty_min:
|
||||||
|
try:
|
||||||
|
status_qs = status_qs.filter(_annotated_difficulty__gte=float(difficulty_min))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if difficulty_max:
|
||||||
|
try:
|
||||||
|
status_qs = status_qs.filter(_annotated_difficulty__lte=float(difficulty_max))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if volume_min:
|
||||||
|
try:
|
||||||
|
status_qs = status_qs.filter(_annotated_volume__gte=int(volume_min))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if volume_max:
|
||||||
|
try:
|
||||||
|
status_qs = status_qs.filter(_annotated_volume__lte=int(volume_max))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if search:
|
||||||
|
status_qs = status_qs.filter(
|
||||||
|
Q(name__icontains=search) | Q(description__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
# Get distinct statuses
|
# Get distinct statuses
|
||||||
statuses = list(set(queryset.values_list('status', flat=True)))
|
statuses = list(set(status_qs.values_list('status', flat=True)))
|
||||||
statuses = sorted([s for s in statuses if s])
|
statuses = sorted([s for s in statuses if s])
|
||||||
status_labels = {
|
status_labels = {
|
||||||
'new': 'New',
|
'new': 'New',
|
||||||
@@ -1281,9 +1382,60 @@ class ClusterViewSet(SiteSectorModelViewSet):
|
|||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# ===== GET DIFFICULTY OPTIONS =====
|
||||||
|
# Apply OTHER filters (exclude difficulty) to get valid difficulty options
|
||||||
|
difficulty_qs = queryset
|
||||||
|
if status_filter:
|
||||||
|
difficulty_qs = difficulty_qs.filter(status=status_filter)
|
||||||
|
if volume_min:
|
||||||
|
try:
|
||||||
|
difficulty_qs = difficulty_qs.filter(_annotated_volume__gte=int(volume_min))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if volume_max:
|
||||||
|
try:
|
||||||
|
difficulty_qs = difficulty_qs.filter(_annotated_volume__lte=int(volume_max))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if search:
|
||||||
|
difficulty_qs = difficulty_qs.filter(
|
||||||
|
Q(name__icontains=search) | Q(description__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get raw difficulty values (0-100) from annotated field
|
||||||
|
difficulty_values = difficulty_qs.exclude(_annotated_difficulty__isnull=True).values_list('_annotated_difficulty', flat=True)
|
||||||
|
|
||||||
|
# Map raw difficulty (0-100) to 1-5 scale and find unique values
|
||||||
|
difficulty_levels = set()
|
||||||
|
for d in difficulty_values:
|
||||||
|
if d is not None:
|
||||||
|
if d <= 10:
|
||||||
|
difficulty_levels.add(1)
|
||||||
|
elif d <= 30:
|
||||||
|
difficulty_levels.add(2)
|
||||||
|
elif d <= 50:
|
||||||
|
difficulty_levels.add(3)
|
||||||
|
elif d <= 70:
|
||||||
|
difficulty_levels.add(4)
|
||||||
|
else:
|
||||||
|
difficulty_levels.add(5)
|
||||||
|
|
||||||
|
difficulty_labels = {
|
||||||
|
1: '1 - Very Easy',
|
||||||
|
2: '2 - Easy',
|
||||||
|
3: '3 - Medium',
|
||||||
|
4: '4 - Hard',
|
||||||
|
5: '5 - Very Hard',
|
||||||
|
}
|
||||||
|
difficulty_options = [
|
||||||
|
{'value': str(d), 'label': difficulty_labels[d]}
|
||||||
|
for d in sorted(difficulty_levels)
|
||||||
|
]
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
data={
|
data={
|
||||||
'statuses': status_options,
|
'statuses': status_options,
|
||||||
|
'difficulties': difficulty_options,
|
||||||
},
|
},
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
@@ -1476,18 +1628,40 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
|
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
|
||||||
def filter_options(self, request):
|
def filter_options(self, request):
|
||||||
"""
|
"""
|
||||||
Get distinct filter values from current data.
|
Get distinct filter values from current data with cascading filter support.
|
||||||
Returns only values that exist in the current site's content ideas.
|
Returns only values that exist based on other active filters.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
# Get distinct statuses
|
# Get filter parameters for cascading
|
||||||
statuses = list(set(queryset.values_list('status', flat=True)))
|
status_filter = request.query_params.get('status', '')
|
||||||
|
content_type_filter = request.query_params.get('content_type', '')
|
||||||
|
content_structure_filter = request.query_params.get('content_structure', '')
|
||||||
|
cluster_filter = request.query_params.get('cluster', '')
|
||||||
|
search = request.query_params.get('search', '')
|
||||||
|
|
||||||
|
# Apply search filter to all options if provided
|
||||||
|
base_qs = queryset
|
||||||
|
if search:
|
||||||
|
base_qs = base_qs.filter(
|
||||||
|
Q(idea_title__icontains=search) | Q(description__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get statuses (filtered by type, structure, cluster)
|
||||||
|
status_qs = base_qs
|
||||||
|
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 cluster_filter:
|
||||||
|
status_qs = status_qs.filter(keyword_cluster_id=cluster_filter)
|
||||||
|
statuses = list(set(status_qs.values_list('status', flat=True)))
|
||||||
statuses = sorted([s for s in statuses if s])
|
statuses = sorted([s for s in statuses if s])
|
||||||
status_labels = {
|
status_labels = {
|
||||||
'new': 'New',
|
'new': 'New',
|
||||||
@@ -1499,8 +1673,15 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
for s in statuses
|
for s in statuses
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct content_types
|
# Get content_types (filtered by status, structure, cluster)
|
||||||
content_types = list(set(queryset.values_list('content_type', flat=True)))
|
type_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
type_qs = type_qs.filter(status=status_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
type_qs = type_qs.filter(content_structure=content_structure_filter)
|
||||||
|
if cluster_filter:
|
||||||
|
type_qs = type_qs.filter(keyword_cluster_id=cluster_filter)
|
||||||
|
content_types = list(set(type_qs.values_list('content_type', flat=True)))
|
||||||
content_types = sorted([t for t in content_types if t])
|
content_types = sorted([t for t in content_types if t])
|
||||||
type_labels = {
|
type_labels = {
|
||||||
'post': 'Post',
|
'post': 'Post',
|
||||||
@@ -1513,8 +1694,15 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
for t in content_types
|
for t in content_types
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct content_structures
|
# Get content_structures (filtered by status, type, cluster)
|
||||||
structures = list(set(queryset.values_list('content_structure', flat=True)))
|
structure_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
structure_qs = structure_qs.filter(status=status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
structure_qs = structure_qs.filter(content_type=content_type_filter)
|
||||||
|
if cluster_filter:
|
||||||
|
structure_qs = structure_qs.filter(keyword_cluster_id=cluster_filter)
|
||||||
|
structures = list(set(structure_qs.values_list('content_structure', flat=True)))
|
||||||
structures = sorted([s for s in structures if s])
|
structures = sorted([s for s in structures if s])
|
||||||
structure_labels = {
|
structure_labels = {
|
||||||
'article': 'Article', 'guide': 'Guide', 'comparison': 'Comparison',
|
'article': 'Article', 'guide': 'Guide', 'comparison': 'Comparison',
|
||||||
@@ -1529,9 +1717,16 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
|||||||
for s in structures
|
for s in structures
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get distinct clusters with ideas
|
# Get distinct clusters (filtered by status, type, structure)
|
||||||
|
cluster_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(status=status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(content_type=content_type_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(content_structure=content_structure_filter)
|
||||||
cluster_ids = list(set(
|
cluster_ids = list(set(
|
||||||
queryset.exclude(keyword_cluster_id__isnull=True)
|
cluster_qs.exclude(keyword_cluster_id__isnull=True)
|
||||||
.values_list('keyword_cluster_id', flat=True)
|
.values_list('keyword_cluster_id', flat=True)
|
||||||
))
|
))
|
||||||
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
||||||
|
|||||||
@@ -273,6 +273,167 @@ class TasksViewSet(SiteSectorModelViewSet):
|
|||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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 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 filter parameters for cascading
|
||||||
|
status_filter = request.query_params.get('status', '')
|
||||||
|
content_type_filter = request.query_params.get('content_type', '')
|
||||||
|
content_structure_filter = request.query_params.get('content_structure', '')
|
||||||
|
cluster_filter = request.query_params.get('cluster', '')
|
||||||
|
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(keywords__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get statuses (filtered by other fields)
|
||||||
|
status_qs = base_qs
|
||||||
|
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 cluster_filter:
|
||||||
|
status_qs = status_qs.filter(cluster_id=cluster_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 = {
|
||||||
|
'queued': 'Queued',
|
||||||
|
'processing': 'Processing',
|
||||||
|
'completed': 'Completed',
|
||||||
|
'failed': 'Failed',
|
||||||
|
}
|
||||||
|
status_options = [
|
||||||
|
{'value': s, 'label': status_labels.get(s, s.title())}
|
||||||
|
for s in statuses
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get content types (filtered by other fields)
|
||||||
|
type_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
type_qs = type_qs.filter(status=status_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
type_qs = type_qs.filter(content_structure=content_structure_filter)
|
||||||
|
if cluster_filter:
|
||||||
|
type_qs = type_qs.filter(cluster_id=cluster_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',
|
||||||
|
'page': 'Page',
|
||||||
|
'product': 'Product',
|
||||||
|
'taxonomy': 'Taxonomy',
|
||||||
|
}
|
||||||
|
content_type_options = [
|
||||||
|
{'value': t, 'label': type_labels.get(t, t.title())}
|
||||||
|
for t in content_types
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get content structures (filtered by other fields)
|
||||||
|
structure_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
structure_qs = structure_qs.filter(status=status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
structure_qs = structure_qs.filter(content_type=content_type_filter)
|
||||||
|
if cluster_filter:
|
||||||
|
structure_qs = structure_qs.filter(cluster_id=cluster_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',
|
||||||
|
'review': 'Review', 'listicle': 'Listicle', 'landing_page': 'Landing Page',
|
||||||
|
'business_page': 'Business Page', 'service_page': 'Service Page',
|
||||||
|
'general': 'General', 'cluster_hub': 'Cluster Hub',
|
||||||
|
'product_page': 'Product Page', 'category_archive': 'Category Archive',
|
||||||
|
'tag_archive': 'Tag Archive', 'attribute_archive': 'Attribute Archive',
|
||||||
|
}
|
||||||
|
content_structure_options = [
|
||||||
|
{'value': s, 'label': structure_labels.get(s, s.replace('_', ' ').title())}
|
||||||
|
for s in structures
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get clusters (filtered by other fields)
|
||||||
|
cluster_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(status=status_filter)
|
||||||
|
if content_type_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(content_type=content_type_filter)
|
||||||
|
if content_structure_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(content_structure=content_structure_filter)
|
||||||
|
if source_filter:
|
||||||
|
cluster_qs = cluster_qs.filter(source=source_filter)
|
||||||
|
from igny8_core.modules.planner.models import Clusters
|
||||||
|
cluster_ids = list(set(
|
||||||
|
cluster_qs.exclude(cluster_id__isnull=True)
|
||||||
|
.values_list('cluster_id', flat=True)
|
||||||
|
))
|
||||||
|
clusters = Clusters.objects.filter(id__in=cluster_ids).values('id', 'name').order_by('name')
|
||||||
|
cluster_options = [
|
||||||
|
{'value': str(c['id']), 'label': c['name']}
|
||||||
|
for c in clusters
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get sources (filtered by other fields)
|
||||||
|
source_qs = base_qs
|
||||||
|
if status_filter:
|
||||||
|
source_qs = source_qs.filter(status=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)
|
||||||
|
if cluster_filter:
|
||||||
|
source_qs = source_qs.filter(cluster_id=cluster_filter)
|
||||||
|
sources = list(set(source_qs.values_list('source', flat=True)))
|
||||||
|
sources = sorted([s for s in sources if s])
|
||||||
|
source_labels = {
|
||||||
|
'manual': 'Manual',
|
||||||
|
'planner': 'Planner',
|
||||||
|
'ai': 'AI',
|
||||||
|
}
|
||||||
|
source_options = [
|
||||||
|
{'value': s, 'label': source_labels.get(s, s.title())}
|
||||||
|
for s in sources
|
||||||
|
]
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={
|
||||||
|
'statuses': status_options,
|
||||||
|
'content_types': content_type_options,
|
||||||
|
'content_structures': content_structure_options,
|
||||||
|
'clusters': cluster_options,
|
||||||
|
'sources': source_options,
|
||||||
|
},
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in filter_options: {str(e)}", exc_info=True)
|
||||||
|
return error_response(
|
||||||
|
error=f'Failed to fetch filter options: {str(e)}',
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
list=extend_schema(tags=['Writer']),
|
list=extend_schema(tags=['Writer']),
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ const SelectDropdown: React.FC<SelectDropdownProps> = ({
|
|||||||
const displayText = selectedOption ? selectedOption.label : placeholder;
|
const displayText = selectedOption ? selectedOption.label : placeholder;
|
||||||
const isPlaceholder = !selectedOption;
|
const isPlaceholder = !selectedOption;
|
||||||
|
|
||||||
|
// Calculate minimum width based on longest option text
|
||||||
|
// This ensures dropdown is wide enough for all options from the start
|
||||||
|
const getLongestOptionLength = () => {
|
||||||
|
if (options.length === 0) return placeholder.length;
|
||||||
|
const allTexts = [placeholder, ...options.map(opt => opt.label)];
|
||||||
|
return Math.max(...allTexts.map(text => text.length));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estimate width: ~8px per character + padding (this is approximate)
|
||||||
|
const estimatedMinWidth = Math.min(360, Math.max(120, getLongestOptionLength() * 8 + 40));
|
||||||
|
|
||||||
// Handle click outside
|
// Handle click outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -102,7 +113,8 @@ const SelectDropdown: React.FC<SelectDropdownProps> = ({
|
|||||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className={`igny8-select-styled w-auto min-w-[120px] max-w-[360px] appearance-none rounded-lg border border-gray-300 bg-transparent px-3 pr-10 shadow-theme-xs focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-800 ${
|
style={{ minWidth: `${estimatedMinWidth}px` }}
|
||||||
|
className={`igny8-select-styled w-auto max-w-[360px] appearance-none rounded-lg border border-gray-300 bg-transparent px-3 pr-10 shadow-theme-xs focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-800 ${
|
||||||
className.includes('text-base') ? 'h-11 py-2.5 text-base' : 'h-9 py-2 text-sm'
|
className.includes('text-base') ? 'h-11 py-2.5 text-base' : 'h-9 py-2 text-sm'
|
||||||
} ${
|
} ${
|
||||||
isPlaceholder
|
isPlaceholder
|
||||||
|
|||||||
@@ -282,8 +282,15 @@ export const createClustersPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
{ value: 'new', label: 'New' },
|
// Use dynamic options if loaded (even if empty array)
|
||||||
{ value: 'mapped', label: 'Mapped' },
|
// Only fall back to defaults if statusOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.statusOptions !== undefined
|
||||||
|
? handlers.statusOptions
|
||||||
|
: [
|
||||||
|
{ value: 'new', label: 'New' },
|
||||||
|
{ value: 'mapped', label: 'Mapped' },
|
||||||
|
]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -292,11 +299,18 @@ export const createClustersPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Difficulty' },
|
{ value: '', label: 'All Difficulty' },
|
||||||
{ value: '1', label: '1 - Very Easy' },
|
// Use dynamic options if loaded (even if empty array)
|
||||||
{ value: '2', label: '2 - Easy' },
|
// Only fall back to defaults if difficultyOptions is undefined (not loaded yet)
|
||||||
{ value: '3', label: '3 - Medium' },
|
...(handlers.difficultyOptions !== undefined
|
||||||
{ value: '4', label: '4 - Hard' },
|
? handlers.difficultyOptions
|
||||||
{ value: '5', label: '5 - Very Hard' },
|
: [
|
||||||
|
{ value: '1', label: '1 - Very Easy' },
|
||||||
|
{ value: '2', label: '2 - Easy' },
|
||||||
|
{ value: '3', label: '3 - Medium' },
|
||||||
|
{ value: '4', label: '4 - Hard' },
|
||||||
|
{ value: '5', label: '5 - Very Hard' },
|
||||||
|
]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -304,7 +318,7 @@ export const createClustersPageConfig = (
|
|||||||
label: 'Volume Range',
|
label: 'Volume Range',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
customRender: () => (
|
customRender: () => (
|
||||||
<div className="relative flex-1 min-w-[140px]">
|
<div className="relative" style={{ minWidth: '180px' }}>
|
||||||
<button
|
<button
|
||||||
ref={handlers.volumeButtonRef}
|
ref={handlers.volumeButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -323,7 +337,7 @@ export const createClustersPageConfig = (
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="block text-left truncate">
|
<span className="block text-left truncate whitespace-nowrap">
|
||||||
{handlers.volumeMin || handlers.volumeMax
|
{handlers.volumeMin || handlers.volumeMax
|
||||||
? `Vol: ${handlers.volumeMin || 'Min'} - ${handlers.volumeMax || 'Max'}`
|
? `Vol: ${handlers.volumeMin || 'Min'} - ${handlers.volumeMax || 'Max'}`
|
||||||
: 'Volume Range'}
|
: 'Volume Range'}
|
||||||
@@ -384,16 +398,14 @@ export const createClustersPageConfig = (
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const newMin = handlers.tempVolumeMin === '' ? '' : Number(handlers.tempVolumeMin);
|
const newMin = handlers.tempVolumeMin === '' ? '' : Number(handlers.tempVolumeMin);
|
||||||
const newMax = handlers.tempVolumeMax === '' ? '' : Number(handlers.tempVolumeMax);
|
const newMax = handlers.tempVolumeMax === '' ? '' : Number(handlers.tempVolumeMax);
|
||||||
handlers.setIsVolumeDropdownOpen(false);
|
handlers.setIsVolumeDropdownOpen(false);
|
||||||
handlers.setVolumeMin(newMin);
|
handlers.setVolumeMin(newMin);
|
||||||
handlers.setVolumeMax(newMax);
|
handlers.setVolumeMax(newMax);
|
||||||
handlers.setCurrentPage(1);
|
handlers.setCurrentPage(1);
|
||||||
setTimeout(() => {
|
// Remove manual loadClusters call - let useEffect handle it automatically
|
||||||
handlers.loadClusters();
|
|
||||||
}, 0);
|
|
||||||
}}
|
}}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -83,6 +83,18 @@ export const createContentPageConfig = (
|
|||||||
setStatusFilter: (value: string) => void;
|
setStatusFilter: (value: string) => void;
|
||||||
setCurrentPage: (page: number) => void;
|
setCurrentPage: (page: number) => void;
|
||||||
onRowClick?: (row: Content) => void;
|
onRowClick?: (row: Content) => void;
|
||||||
|
// Dynamic filter options
|
||||||
|
statusOptions?: Array<{ value: string; label: string }>;
|
||||||
|
sourceOptions?: Array<{ value: string; label: string }>;
|
||||||
|
contentTypeOptions?: Array<{ value: string; label: string }>;
|
||||||
|
contentStructureOptions?: Array<{ value: string; label: string }>;
|
||||||
|
// New filter setters
|
||||||
|
contentTypeFilter?: string;
|
||||||
|
setContentTypeFilter?: (value: string) => void;
|
||||||
|
contentStructureFilter?: string;
|
||||||
|
setContentStructureFilter?: (value: string) => void;
|
||||||
|
sourceFilter?: string;
|
||||||
|
setSourceFilter?: (value: string) => void;
|
||||||
}
|
}
|
||||||
): ContentPageConfig => {
|
): ContentPageConfig => {
|
||||||
const showSectorColumn = !handlers.activeSector;
|
const showSectorColumn = !handlers.activeSector;
|
||||||
@@ -404,8 +416,15 @@ export const createContentPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
{ value: 'draft', label: 'Draft' },
|
...(handlers.statusOptions !== undefined
|
||||||
{ value: 'published', label: 'Published' },
|
? handlers.statusOptions
|
||||||
|
: [
|
||||||
|
{ value: 'draft', label: 'Draft' },
|
||||||
|
{ value: 'review', label: 'Review' },
|
||||||
|
{ value: 'approved', label: 'Approved' },
|
||||||
|
{ value: 'published', label: 'Published' },
|
||||||
|
]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -445,10 +464,16 @@ export const createContentPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Sources' },
|
{ value: '', label: 'All Sources' },
|
||||||
{ value: 'igny8', label: 'IGNY8' },
|
...(handlers.sourceOptions !== undefined
|
||||||
{ value: 'wordpress', label: 'WordPress' },
|
? handlers.sourceOptions
|
||||||
|
: [
|
||||||
|
{ value: 'igny8', label: 'IGNY8' },
|
||||||
|
{ value: 'wordpress', label: 'WordPress' },
|
||||||
|
]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
headerMetrics: [
|
headerMetrics: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,7 +238,9 @@ export const createIdeasPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
...(handlers.statusOptions && handlers.statusOptions.length > 0
|
// Use dynamic options if loaded (even if empty array)
|
||||||
|
// Only fall back to defaults if statusOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.statusOptions !== undefined
|
||||||
? handlers.statusOptions
|
? handlers.statusOptions
|
||||||
: [
|
: [
|
||||||
{ value: 'new', label: 'New' },
|
{ value: 'new', label: 'New' },
|
||||||
@@ -254,7 +256,9 @@ export const createIdeasPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Structures' },
|
{ value: '', label: 'All Structures' },
|
||||||
...(handlers.contentStructureOptions && handlers.contentStructureOptions.length > 0
|
// Use dynamic options if loaded (even if empty array)
|
||||||
|
// Only fall back to defaults if contentStructureOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.contentStructureOptions !== undefined
|
||||||
? handlers.contentStructureOptions
|
? handlers.contentStructureOptions
|
||||||
: [
|
: [
|
||||||
{ value: 'article', label: 'Article' },
|
{ value: 'article', label: 'Article' },
|
||||||
@@ -281,7 +285,9 @@ export const createIdeasPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Types' },
|
{ value: '', label: 'All Types' },
|
||||||
...(handlers.contentTypeOptions && handlers.contentTypeOptions.length > 0
|
// Use dynamic options if loaded (even if empty array)
|
||||||
|
// Only fall back to defaults if contentTypeOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.contentTypeOptions !== undefined
|
||||||
? handlers.contentTypeOptions
|
? handlers.contentTypeOptions
|
||||||
: [
|
: [
|
||||||
{ value: 'post', label: 'Post' },
|
{ value: 'post', label: 'Post' },
|
||||||
@@ -299,7 +305,9 @@ export const createIdeasPageConfig = (
|
|||||||
dynamicOptions: 'clusters',
|
dynamicOptions: 'clusters',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Clusters' },
|
{ value: '', label: 'All Clusters' },
|
||||||
...(handlers.clusterOptions && handlers.clusterOptions.length > 0
|
// Use dynamic cluster options if loaded (even if empty array)
|
||||||
|
// Only fall back to full clusters list if clusterOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.clusterOptions !== undefined
|
||||||
? handlers.clusterOptions
|
? handlers.clusterOptions
|
||||||
: handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name }))
|
: handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name }))
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -272,8 +272,9 @@ export const createKeywordsPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
// Use dynamic options if available, otherwise show default options
|
// Use dynamic options if loaded (even if empty array)
|
||||||
...(handlers.statusOptions && handlers.statusOptions.length > 0
|
// Only fall back to defaults if statusOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.statusOptions !== undefined
|
||||||
? handlers.statusOptions
|
? handlers.statusOptions
|
||||||
: [
|
: [
|
||||||
{ value: 'new', label: 'New' },
|
{ value: 'new', label: 'New' },
|
||||||
@@ -288,8 +289,9 @@ export const createKeywordsPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Countries' },
|
{ value: '', label: 'All Countries' },
|
||||||
// Use dynamic options if available, otherwise show default options
|
// Use dynamic options if loaded (even if empty array)
|
||||||
...(handlers.countryOptions && handlers.countryOptions.length > 0
|
// Only fall back to defaults if countryOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.countryOptions !== undefined
|
||||||
? handlers.countryOptions
|
? handlers.countryOptions
|
||||||
: [
|
: [
|
||||||
{ value: 'US', label: 'United States' },
|
{ value: 'US', label: 'United States' },
|
||||||
@@ -309,7 +311,9 @@ export const createKeywordsPageConfig = (
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Difficulty' },
|
{ value: '', label: 'All Difficulty' },
|
||||||
...(handlers.difficultyOptions && handlers.difficultyOptions.length > 0
|
// Use dynamic options if loaded (even if empty array)
|
||||||
|
// Only fall back to defaults if difficultyOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.difficultyOptions !== undefined
|
||||||
? handlers.difficultyOptions
|
? handlers.difficultyOptions
|
||||||
: [
|
: [
|
||||||
{ value: '1', label: '1 - Very Easy' },
|
{ value: '1', label: '1 - Very Easy' },
|
||||||
@@ -328,8 +332,9 @@ export const createKeywordsPageConfig = (
|
|||||||
dynamicOptions: 'clusters', // Flag for dynamic option loading
|
dynamicOptions: 'clusters', // Flag for dynamic option loading
|
||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Clusters' },
|
{ value: '', label: 'All Clusters' },
|
||||||
// Use dynamic cluster options if available
|
// Use dynamic cluster options if loaded (even if empty array)
|
||||||
...(handlers.clusterOptions && handlers.clusterOptions.length > 0
|
// Only fall back to full clusters list if clusterOptions is undefined (not loaded yet)
|
||||||
|
...(handlers.clusterOptions !== undefined
|
||||||
? handlers.clusterOptions
|
? handlers.clusterOptions
|
||||||
: handlers.clusters.map(c => ({ value: String(c.id), label: c.name }))
|
: handlers.clusters.map(c => ({ value: String(c.id), label: c.name }))
|
||||||
),
|
),
|
||||||
@@ -340,7 +345,7 @@ export const createKeywordsPageConfig = (
|
|||||||
label: 'Volume Range',
|
label: 'Volume Range',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
customRender: () => (
|
customRender: () => (
|
||||||
<div className="relative flex-1 min-w-[140px]">
|
<div className="relative" style={{ minWidth: '180px' }}>
|
||||||
<button
|
<button
|
||||||
ref={handlers.volumeButtonRef}
|
ref={handlers.volumeButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -359,7 +364,7 @@ export const createKeywordsPageConfig = (
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="block text-left truncate">
|
<span className="block text-left truncate whitespace-nowrap">
|
||||||
{handlers.volumeMin || handlers.volumeMax
|
{handlers.volumeMin || handlers.volumeMax
|
||||||
? `Vol: ${handlers.volumeMin || 'Min'} - ${handlers.volumeMax || 'Max'}`
|
? `Vol: ${handlers.volumeMin || 'Min'} - ${handlers.volumeMax || 'Max'}`
|
||||||
: 'Volume Range'}
|
: 'Volume Range'}
|
||||||
@@ -420,16 +425,14 @@ export const createKeywordsPageConfig = (
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
const newMin = handlers.tempVolumeMin === '' ? '' : Number(handlers.tempVolumeMin);
|
const newMin = handlers.tempVolumeMin === '' ? '' : Number(handlers.tempVolumeMin);
|
||||||
const newMax = handlers.tempVolumeMax === '' ? '' : Number(handlers.tempVolumeMax);
|
const newMax = handlers.tempVolumeMax === '' ? '' : Number(handlers.tempVolumeMax);
|
||||||
handlers.setIsVolumeDropdownOpen(false);
|
handlers.setIsVolumeDropdownOpen(false);
|
||||||
handlers.setVolumeMin(newMin);
|
handlers.setVolumeMin(newMin);
|
||||||
handlers.setVolumeMax(newMax);
|
handlers.setVolumeMax(newMax);
|
||||||
handlers.setCurrentPage(1);
|
handlers.setCurrentPage(1);
|
||||||
setTimeout(() => {
|
// Remove manual loadKeywords call - let useEffect handle it automatically
|
||||||
handlers.loadKeywords();
|
|
||||||
}, 0);
|
|
||||||
}}
|
}}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ export function createReviewPageConfig(params: {
|
|||||||
setCurrentPage: (page: number) => void;
|
setCurrentPage: (page: number) => void;
|
||||||
activeSector: { id: number; name: string } | null;
|
activeSector: { id: number; name: string } | null;
|
||||||
onRowClick?: (row: Content) => void;
|
onRowClick?: (row: Content) => void;
|
||||||
|
// Dynamic filter options
|
||||||
|
statusOptions?: Array<{ value: string; label: string }>;
|
||||||
|
siteStatusOptions?: Array<{ value: string; label: string }>;
|
||||||
|
contentTypeOptions?: Array<{ value: string; label: string }>;
|
||||||
|
contentStructureOptions?: Array<{ value: string; label: string }>;
|
||||||
|
// Filter values and setters
|
||||||
|
siteStatusFilter?: string;
|
||||||
|
setSiteStatusFilter?: (value: string) => void;
|
||||||
|
contentTypeFilter?: string;
|
||||||
|
setContentTypeFilter?: (value: string) => void;
|
||||||
|
contentStructureFilter?: string;
|
||||||
|
setContentStructureFilter?: (value: string) => void;
|
||||||
}): ReviewPageConfig {
|
}): ReviewPageConfig {
|
||||||
const showSectorColumn = !params.activeSector;
|
const showSectorColumn = !params.activeSector;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Cluster,
|
Cluster,
|
||||||
ClusterFilters,
|
ClusterFilters,
|
||||||
ClusterCreateData,
|
ClusterCreateData,
|
||||||
|
FilterOption,
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import FormModal from '../../components/common/FormModal';
|
import FormModal from '../../components/common/FormModal';
|
||||||
import ProgressModal from '../../components/common/ProgressModal';
|
import ProgressModal from '../../components/common/ProgressModal';
|
||||||
@@ -28,7 +29,6 @@ import { createClustersPageConfig } from '../../config/pages/clusters.config';
|
|||||||
import { useSectorStore } from '../../store/sectorStore';
|
import { useSectorStore } from '../../store/sectorStore';
|
||||||
import { useSiteStore } from '../../store/siteStore';
|
import { useSiteStore } from '../../store/siteStore';
|
||||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
|
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
|
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
|
||||||
|
|
||||||
@@ -49,6 +49,11 @@ export default function Clusters() {
|
|||||||
const [totalVolume, setTotalVolume] = useState(0);
|
const [totalVolume, setTotalVolume] = useState(0);
|
||||||
const [totalKeywords, setTotalKeywords] = useState(0);
|
const [totalKeywords, setTotalKeywords] = useState(0);
|
||||||
|
|
||||||
|
// Dynamic filter options (loaded from backend based on current data)
|
||||||
|
// Initialize as undefined to distinguish "not loaded yet" from "loaded but empty array"
|
||||||
|
const [statusOptions, setStatusOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
|
const [difficultyOptions, setDifficultyOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
@@ -86,6 +91,74 @@ export default function Clusters() {
|
|||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
const hasReloadedRef = useRef(false);
|
const hasReloadedRef = useRef(false);
|
||||||
|
|
||||||
|
// Load dynamic filter options based on current site's data
|
||||||
|
const loadFilterOptions = useCallback(async () => {
|
||||||
|
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 || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading filter options:', error);
|
||||||
|
}
|
||||||
|
}, [activeSite]);
|
||||||
|
|
||||||
|
// Load filter options when site changes
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions();
|
||||||
|
}, [loadFilterOptions]);
|
||||||
|
|
||||||
|
// Reload filter options when filters change (cascading)
|
||||||
|
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<number, { min: number; max: number }> = {
|
||||||
|
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]);
|
||||||
|
|
||||||
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -145,13 +218,18 @@ export default function Clusters() {
|
|||||||
// Add difficulty range filter
|
// Add difficulty range filter
|
||||||
if (difficultyFilter) {
|
if (difficultyFilter) {
|
||||||
const difficultyNum = parseInt(difficultyFilter);
|
const difficultyNum = parseInt(difficultyFilter);
|
||||||
const label = getDifficultyLabelFromNumber(difficultyNum);
|
// Map difficulty level (1-5) directly to raw difficulty range (0-100)
|
||||||
if (label !== null) {
|
const ranges: Record<number, { min: number; max: number }> = {
|
||||||
const range = getDifficultyRange(label);
|
1: { min: 0, max: 10 },
|
||||||
if (range) {
|
2: { min: 11, max: 30 },
|
||||||
filters.difficulty_min = range.min;
|
3: { min: 31, max: 50 },
|
||||||
filters.difficulty_max = range.max;
|
4: { min: 51, max: 70 },
|
||||||
}
|
5: { min: 71, max: 100 },
|
||||||
|
};
|
||||||
|
const range = ranges[difficultyNum];
|
||||||
|
if (range) {
|
||||||
|
filters.difficulty_min = range.min;
|
||||||
|
filters.difficulty_max = range.max;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +460,9 @@ export default function Clusters() {
|
|||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
loadClusters,
|
loadClusters,
|
||||||
onGenerateIdeas: (clusterId: number) => handleRowAction('generate_ideas', { id: clusterId } as Cluster),
|
onGenerateIdeas: (clusterId: number) => handleRowAction('generate_ideas', { id: clusterId } as Cluster),
|
||||||
|
// Dynamic filter options
|
||||||
|
statusOptions,
|
||||||
|
difficultyOptions,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
activeSector,
|
activeSector,
|
||||||
@@ -395,6 +476,8 @@ export default function Clusters() {
|
|||||||
tempVolumeMin,
|
tempVolumeMin,
|
||||||
tempVolumeMax,
|
tempVolumeMax,
|
||||||
loadClusters,
|
loadClusters,
|
||||||
|
statusOptions,
|
||||||
|
difficultyOptions,
|
||||||
handleRowAction,
|
handleRowAction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ export default function Ideas() {
|
|||||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||||
|
|
||||||
// Dynamic filter options
|
// Dynamic filter options
|
||||||
const [statusOptions, setStatusOptions] = useState<FilterOption[]>([]);
|
// Initialize as undefined to distinguish "not loaded yet" from "loaded but empty array"
|
||||||
const [contentTypeOptions, setContentTypeOptions] = useState<FilterOption[]>([]);
|
const [statusOptions, setStatusOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
const [contentStructureOptions, setContentStructureOptions] = useState<FilterOption[]>([]);
|
const [contentTypeOptions, setContentTypeOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
const [clusterOptions, setClusterOptions] = useState<FilterOption[]>([]);
|
const [contentStructureOptions, setContentStructureOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
|
const [clusterOptions, setClusterOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -126,6 +127,38 @@ export default function Ideas() {
|
|||||||
loadFilterOptions();
|
loadFilterOptions();
|
||||||
}, [loadFilterOptions]);
|
}, [loadFilterOptions]);
|
||||||
|
|
||||||
|
// Reload filter options when filters change (cascading)
|
||||||
|
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]);
|
||||||
|
|
||||||
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -185,6 +218,7 @@ export default function Ideas() {
|
|||||||
...(clusterFilter && { keyword_cluster_id: clusterFilter }),
|
...(clusterFilter && { keyword_cluster_id: clusterFilter }),
|
||||||
...(structureFilter && { content_structure: structureFilter }),
|
...(structureFilter && { content_structure: structureFilter }),
|
||||||
...(typeFilter && { content_type: typeFilter }),
|
...(typeFilter && { content_type: typeFilter }),
|
||||||
|
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
ordering,
|
ordering,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { useSectorStore } from '../../store/sectorStore';
|
|||||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
|
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
|
||||||
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
|
import { getDifficultyNumber } from '../../utils/difficulty';
|
||||||
import FormModal from '../../components/common/FormModal';
|
import FormModal from '../../components/common/FormModal';
|
||||||
import ProgressModal from '../../components/common/ProgressModal';
|
import ProgressModal from '../../components/common/ProgressModal';
|
||||||
import { useProgressModal } from '../../hooks/useProgressModal';
|
import { useProgressModal } from '../../hooks/useProgressModal';
|
||||||
@@ -57,10 +57,11 @@ export default function Keywords() {
|
|||||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||||
|
|
||||||
// Dynamic filter options (loaded from backend based on current data)
|
// Dynamic filter options (loaded from backend based on current data)
|
||||||
const [countryOptions, setCountryOptions] = useState<FilterOption[]>([]);
|
// Initialize as undefined to distinguish "not loaded yet" from "loaded but empty array"
|
||||||
const [statusOptions, setStatusOptions] = useState<FilterOption[]>([]);
|
const [countryOptions, setCountryOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
const [clusterOptions, setClusterOptions] = useState<FilterOption[]>([]);
|
const [statusOptions, setStatusOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
const [difficultyOptions, setDifficultyOptions] = useState<FilterOption[]>([]);
|
const [clusterOptions, setClusterOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
|
const [difficultyOptions, setDifficultyOptions] = useState<FilterOption[] | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state - match Keywords.tsx
|
// Filter state - match Keywords.tsx
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -133,6 +134,9 @@ export default function Keywords() {
|
|||||||
cluster_id?: string;
|
cluster_id?: string;
|
||||||
difficulty_min?: number;
|
difficulty_min?: number;
|
||||||
difficulty_max?: number;
|
difficulty_max?: number;
|
||||||
|
volume_min?: number;
|
||||||
|
volume_max?: number;
|
||||||
|
search?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
@@ -178,8 +182,11 @@ export default function Keywords() {
|
|||||||
cluster_id: clusterFilter || undefined,
|
cluster_id: clusterFilter || undefined,
|
||||||
difficulty_min: difficultyMin,
|
difficulty_min: difficultyMin,
|
||||||
difficulty_max: difficultyMax,
|
difficulty_max: difficultyMax,
|
||||||
|
volume_min: volumeMin !== '' ? Number(volumeMin) : undefined,
|
||||||
|
volume_max: volumeMax !== '' ? Number(volumeMax) : undefined,
|
||||||
|
search: searchTerm || undefined,
|
||||||
});
|
});
|
||||||
}, [statusFilter, countryFilter, clusterFilter, difficultyFilter, loadFilterOptions, getDifficultyRange]);
|
}, [statusFilter, countryFilter, clusterFilter, difficultyFilter, volumeMin, volumeMax, searchTerm, loadFilterOptions, getDifficultyRange]);
|
||||||
|
|
||||||
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
// Load total metrics for footer widget (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
@@ -251,13 +258,18 @@ export default function Keywords() {
|
|||||||
// Add difficulty range filter
|
// Add difficulty range filter
|
||||||
if (difficultyFilter) {
|
if (difficultyFilter) {
|
||||||
const difficultyNum = parseInt(difficultyFilter);
|
const difficultyNum = parseInt(difficultyFilter);
|
||||||
const label = getDifficultyLabelFromNumber(difficultyNum);
|
// Map difficulty level (1-5) directly to raw difficulty range (0-100)
|
||||||
if (label !== null) {
|
const ranges: Record<number, { min: number; max: number }> = {
|
||||||
const range = getDifficultyRange(label);
|
1: { min: 0, max: 10 },
|
||||||
if (range) {
|
2: { min: 11, max: 30 },
|
||||||
filters.difficulty_min = range.min;
|
3: { min: 31, max: 50 },
|
||||||
filters.difficulty_max = range.max;
|
4: { min: 51, max: 70 },
|
||||||
}
|
5: { min: 71, max: 100 },
|
||||||
|
};
|
||||||
|
const range = ranges[difficultyNum];
|
||||||
|
if (range) {
|
||||||
|
filters.difficulty_min = range.min;
|
||||||
|
filters.difficulty_max = range.max;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export default function Approved() {
|
|||||||
const [totalPublished, setTotalPublished] = useState(0);
|
const [totalPublished, setTotalPublished] = useState(0);
|
||||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||||
|
|
||||||
|
// Dynamic filter options (loaded from backend)
|
||||||
|
const [statusOptions, setStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [siteStatusOptions, setSiteStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentTypeOptions, setContentTypeOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentStructureOptions, setContentStructureOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState(''); // Status filter (draft/review/approved/published)
|
const [statusFilter, setStatusFilter] = useState(''); // Status filter (draft/review/approved/published)
|
||||||
@@ -63,6 +69,39 @@ export default function Approved() {
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
|
// Load dynamic filter options with cascading
|
||||||
|
const loadFilterOptions = useCallback(async () => {
|
||||||
|
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 || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading filter options:', error);
|
||||||
|
}
|
||||||
|
}, [activeSite, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm]);
|
||||||
|
|
||||||
|
// Load filter options when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions();
|
||||||
|
}, [loadFilterOptions]);
|
||||||
|
|
||||||
// Load total metrics for footer widget and header metrics (not affected by pagination)
|
// Load total metrics for footer widget and header metrics (not affected by pagination)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,10 +46,18 @@ export default function Content() {
|
|||||||
const [totalPublished, setTotalPublished] = useState(0);
|
const [totalPublished, setTotalPublished] = useState(0);
|
||||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||||
|
|
||||||
|
// Dynamic filter options (loaded from backend)
|
||||||
|
const [statusOptions, setStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [sourceOptions, setSourceOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentTypeOptions, setContentTypeOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentStructureOptions, setContentStructureOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('draft');
|
const [statusFilter, setStatusFilter] = useState('draft');
|
||||||
const [sourceFilter, setSourceFilter] = useState('');
|
const [sourceFilter, setSourceFilter] = useState('');
|
||||||
|
const [contentTypeFilter, setContentTypeFilter] = useState('');
|
||||||
|
const [contentStructureFilter, setContentStructureFilter] = useState('');
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
@@ -66,6 +74,39 @@ export default function Content() {
|
|||||||
const progressModal = useProgressModal();
|
const progressModal = useProgressModal();
|
||||||
const hasReloadedRef = useRef(false);
|
const hasReloadedRef = useRef(false);
|
||||||
|
|
||||||
|
// Load dynamic filter options
|
||||||
|
const loadFilterOptions = useCallback(async () => {
|
||||||
|
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 || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading filter options:', error);
|
||||||
|
}
|
||||||
|
}, [activeSite, statusFilter, sourceFilter, contentTypeFilter, contentStructureFilter, searchTerm]);
|
||||||
|
|
||||||
|
// Load filter options when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions();
|
||||||
|
}, [loadFilterOptions]);
|
||||||
|
|
||||||
// Load total metrics for footer widget and header metrics (site-wide totals, no sector filter)
|
// Load total metrics for footer widget and header metrics (site-wide totals, no sector filter)
|
||||||
const loadTotalMetrics = useCallback(async () => {
|
const loadTotalMetrics = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -131,6 +172,8 @@ export default function Content() {
|
|||||||
...(searchTerm && { search: searchTerm }),
|
...(searchTerm && { search: searchTerm }),
|
||||||
...(statusFilter && { status: statusFilter }),
|
...(statusFilter && { status: statusFilter }),
|
||||||
...(sourceFilter && { source: sourceFilter }),
|
...(sourceFilter && { source: sourceFilter }),
|
||||||
|
...(contentTypeFilter && { content_type: contentTypeFilter }),
|
||||||
|
...(contentStructureFilter && { content_structure: contentStructureFilter }),
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
ordering,
|
ordering,
|
||||||
@@ -151,7 +194,7 @@ export default function Content() {
|
|||||||
setShowContent(true);
|
setShowContent(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize, toast]);
|
}, [currentPage, statusFilter, sourceFilter, contentTypeFilter, contentStructureFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize, toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadContent();
|
loadContent();
|
||||||
@@ -216,13 +259,32 @@ export default function Content() {
|
|||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
|
// Dynamic filter options
|
||||||
|
statusOptions,
|
||||||
|
sourceOptions,
|
||||||
|
contentTypeOptions,
|
||||||
|
contentStructureOptions,
|
||||||
|
// Filter values and setters
|
||||||
|
contentTypeFilter,
|
||||||
|
setContentTypeFilter,
|
||||||
|
contentStructureFilter,
|
||||||
|
setContentStructureFilter,
|
||||||
|
sourceFilter,
|
||||||
|
setSourceFilter,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
activeSector,
|
activeSector,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
handleRowClick,
|
handleRowClick,
|
||||||
]);
|
statusOptions,
|
||||||
|
sourceOptions,
|
||||||
|
contentTypeOptions,
|
||||||
|
contentStructureOptions,
|
||||||
|
contentTypeFilter,
|
||||||
|
contentStructureFilter,
|
||||||
|
sourceFilter,
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate header metrics - use totals from API calls (not page data)
|
// Calculate header metrics - use totals from API calls (not page data)
|
||||||
// This ensures metrics show correct totals across all pages, not just current page
|
// This ensures metrics show correct totals across all pages, not just current page
|
||||||
|
|||||||
@@ -43,9 +43,18 @@ export default function Review() {
|
|||||||
const [totalPublished, setTotalPublished] = useState(0);
|
const [totalPublished, setTotalPublished] = useState(0);
|
||||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||||
|
|
||||||
|
// Dynamic filter options (loaded from backend)
|
||||||
|
const [statusOptions, setStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [siteStatusOptions, setSiteStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentTypeOptions, setContentTypeOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentStructureOptions, setContentStructureOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state - default to review status
|
// Filter state - default to review status
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('review'); // Default to review
|
const [statusFilter, setStatusFilter] = useState('review'); // Default to review
|
||||||
|
const [siteStatusFilter, setSiteStatusFilter] = useState('');
|
||||||
|
const [contentTypeFilter, setContentTypeFilter] = useState('');
|
||||||
|
const [contentStructureFilter, setContentStructureFilter] = useState('');
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
@@ -58,6 +67,39 @@ export default function Review() {
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
|
// Load dynamic filter options
|
||||||
|
const loadFilterOptions = useCallback(async () => {
|
||||||
|
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 || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading filter options:', error);
|
||||||
|
}
|
||||||
|
}, [activeSite, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm]);
|
||||||
|
|
||||||
|
// Load filter options when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions();
|
||||||
|
}, [loadFilterOptions]);
|
||||||
|
|
||||||
// Load content - filtered for review status
|
// Load content - filtered for review status
|
||||||
const loadContent = useCallback(async () => {
|
const loadContent = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -68,6 +110,9 @@ export default function Review() {
|
|||||||
const filters: ContentFilters = {
|
const filters: ContentFilters = {
|
||||||
...(searchTerm && { search: searchTerm }),
|
...(searchTerm && { search: searchTerm }),
|
||||||
status: 'review', // Always filter for review status
|
status: 'review', // Always filter for review status
|
||||||
|
...(siteStatusFilter && { site_status: siteStatusFilter }),
|
||||||
|
...(contentTypeFilter && { content_type: contentTypeFilter }),
|
||||||
|
...(contentStructureFilter && { content_structure: contentStructureFilter }),
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
ordering,
|
ordering,
|
||||||
@@ -88,7 +133,7 @@ export default function Review() {
|
|||||||
setShowContent(true);
|
setShowContent(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, sortBy, sortDirection, searchTerm, pageSize, toast]);
|
}, [currentPage, siteStatusFilter, contentTypeFilter, contentStructureFilter, sortBy, sortDirection, searchTerm, pageSize, toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadContent();
|
loadContent();
|
||||||
@@ -163,8 +208,32 @@ export default function Review() {
|
|||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
onRowClick: handleRowClick,
|
onRowClick: handleRowClick,
|
||||||
|
// Dynamic filter options
|
||||||
|
statusOptions,
|
||||||
|
siteStatusOptions,
|
||||||
|
contentTypeOptions,
|
||||||
|
contentStructureOptions,
|
||||||
|
// Filter values and setters
|
||||||
|
siteStatusFilter,
|
||||||
|
setSiteStatusFilter,
|
||||||
|
contentTypeFilter,
|
||||||
|
setContentTypeFilter,
|
||||||
|
contentStructureFilter,
|
||||||
|
setContentStructureFilter,
|
||||||
}),
|
}),
|
||||||
[activeSector, searchTerm, statusFilter, handleRowClick]
|
[
|
||||||
|
activeSector,
|
||||||
|
searchTerm,
|
||||||
|
statusFilter,
|
||||||
|
handleRowClick,
|
||||||
|
statusOptions,
|
||||||
|
siteStatusOptions,
|
||||||
|
contentTypeOptions,
|
||||||
|
contentStructureOptions,
|
||||||
|
siteStatusFilter,
|
||||||
|
contentTypeFilter,
|
||||||
|
contentStructureFilter,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Header metrics (calculated from loaded data)
|
// Header metrics (calculated from loaded data)
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ export default function Tasks() {
|
|||||||
const [totalProcessing, setTotalProcessing] = useState(0);
|
const [totalProcessing, setTotalProcessing] = useState(0);
|
||||||
const [totalCompleted, setTotalCompleted] = useState(0);
|
const [totalCompleted, setTotalCompleted] = useState(0);
|
||||||
|
|
||||||
|
// Dynamic filter options (loaded from backend)
|
||||||
|
const [statusOptions, setStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentTypeOptions, setContentTypeOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [contentStructureOptions, setContentStructureOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [clusterOptions, setClusterOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
const [sourceOptions, setSourceOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
@@ -99,6 +106,41 @@ export default function Tasks() {
|
|||||||
|
|
||||||
const hasReloadedRef = useRef<boolean>(false);
|
const hasReloadedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Load dynamic filter options with cascading
|
||||||
|
const loadFilterOptions = useCallback(async () => {
|
||||||
|
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 || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading filter options:', error);
|
||||||
|
}
|
||||||
|
}, [activeSite, statusFilter, typeFilter, structureFilter, clusterFilter, sourceFilter, searchTerm]);
|
||||||
|
|
||||||
|
// Load filter options when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
loadFilterOptions();
|
||||||
|
}, [loadFilterOptions]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Load clusters for filter dropdown
|
// Load clusters for filter dropdown
|
||||||
|
|||||||
@@ -762,6 +762,9 @@ export interface KeywordFilterOptionsRequest {
|
|||||||
difficulty_min?: number;
|
difficulty_min?: number;
|
||||||
difficulty_max?: number;
|
difficulty_max?: number;
|
||||||
cluster_id?: string;
|
cluster_id?: string;
|
||||||
|
volume_min?: number;
|
||||||
|
volume_max?: number;
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlannerKeywordFilterOptions(
|
export async function fetchPlannerKeywordFilterOptions(
|
||||||
@@ -775,6 +778,9 @@ export async function fetchPlannerKeywordFilterOptions(
|
|||||||
if (filters?.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString());
|
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?.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString());
|
||||||
if (filters?.cluster_id) params.append('cluster_id', filters.cluster_id);
|
if (filters?.cluster_id) params.append('cluster_id', filters.cluster_id);
|
||||||
|
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();
|
const queryString = params.toString();
|
||||||
return fetchAPI(`/v1/planner/keywords/filter_options/${queryString ? `?${queryString}` : ''}`);
|
return fetchAPI(`/v1/planner/keywords/filter_options/${queryString ? `?${queryString}` : ''}`);
|
||||||
|
|||||||
@@ -648,16 +648,46 @@ export default function TablePageTemplate({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filter Toggle Button */}
|
{/* Filter Toggle Button - with active filter count badge */}
|
||||||
{(renderFilters || filters.length > 0) && (
|
{(renderFilters || filters.length > 0) && (
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="secondary"
|
<Button
|
||||||
size="sm"
|
variant="secondary"
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
size="sm"
|
||||||
startIcon={<FunnelIcon className="w-3.5 h-3.5" />}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
>
|
startIcon={<FunnelIcon className="w-3.5 h-3.5" />}
|
||||||
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
>
|
||||||
</Button>
|
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
||||||
|
</Button>
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<>
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border border-brand-200 dark:border-brand-700/50 rounded-md">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-brand-500 animate-pulse"></div>
|
||||||
|
<span className="text-xs font-semibold text-brand-700 dark:text-brand-300">
|
||||||
|
{Object.values(filterValues).filter(value => {
|
||||||
|
if (value === '' || value === null || value === undefined) return false;
|
||||||
|
if (typeof value === 'object' && ('min' in value || 'max' in value)) {
|
||||||
|
return value.min !== '' && value.min !== null && value.min !== undefined ||
|
||||||
|
value.max !== '' && value.max !== null && value.max !== undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).length} active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{onFilterReset && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
tone="danger"
|
||||||
|
size="xs"
|
||||||
|
onClick={onFilterReset}
|
||||||
|
className="shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -777,15 +807,6 @@ export default function TablePageTemplate({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
{hasActiveFilters && onFilterReset && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onFilterReset}
|
|
||||||
>
|
|
||||||
Clear Filters
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user