From 8c8f2df5ddfa948ac1c3e12e3789333a8dbeac23 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 19 Jan 2026 09:25:34 +0000 Subject: [PATCH] Filters and badges --- frontend/src/config/pages/approved.config.tsx | 44 +++++++-- frontend/src/config/pages/clusters.config.tsx | 4 +- frontend/src/config/pages/content.config.tsx | 94 ++++++------------- frontend/src/config/pages/ideas.config.tsx | 8 +- frontend/src/config/pages/images.config.tsx | 13 ++- frontend/src/config/pages/keywords.config.tsx | 4 +- frontend/src/config/pages/review.config.tsx | 79 ++++------------ frontend/src/config/pages/tasks.config.tsx | 65 ++++++++----- frontend/src/pages/Planner/Clusters.tsx | 4 +- frontend/src/pages/Planner/Ideas.tsx | 4 +- frontend/src/pages/Planner/Keywords.tsx | 4 +- .../pages/Setup/IndustriesSectorsKeywords.tsx | 14 ++- frontend/src/pages/Writer/Approved.tsx | 9 +- frontend/src/pages/Writer/Content.tsx | 26 +++-- frontend/src/pages/Writer/Images.tsx | 47 +++++++++- frontend/src/pages/Writer/Review.tsx | 32 +++---- frontend/src/pages/Writer/Tasks.tsx | 21 ++--- frontend/src/services/api.ts | 3 - frontend/src/utils/badgeColors.ts | 70 ++++++++++++++ 19 files changed, 322 insertions(+), 223 deletions(-) create mode 100644 frontend/src/utils/badgeColors.ts diff --git a/frontend/src/config/pages/approved.config.tsx b/frontend/src/config/pages/approved.config.tsx index b408b51e..605c07c5 100644 --- a/frontend/src/config/pages/approved.config.tsx +++ b/frontend/src/config/pages/approved.config.tsx @@ -7,7 +7,9 @@ import { Content } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { CheckCircleIcon, ArrowRightIcon } from '../../icons'; -import { STRUCTURE_LABELS, TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping'; +import { TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping'; +import { getSectorBadgeColor, getStructureBadgeColor, getStructureLabel } from '../../utils/badgeColors'; +import type { Sector } from '../../store/sectorStore'; export interface ColumnConfig { key: string; @@ -54,7 +56,12 @@ export function createApprovedPageConfig(params: { setSiteStatusFilter: (value: string) => void; setCurrentPage: (page: number) => void; activeSector: { id: number; name: string } | null; + sectors?: Sector[]; onRowClick?: (row: Content) => void; + statusOptions?: Array<{ value: string; label: string }>; + siteStatusOptions?: Array<{ value: string; label: string }>; + contentTypeOptions?: Array<{ value: string; label: string }>; + contentStructureOptions?: Array<{ value: string; label: string }>; }): ApprovedPageConfig { const showSectorColumn = !params.activeSector; @@ -230,12 +237,12 @@ export function createApprovedPageConfig(params: { sortField: 'content_structure', width: '130px', render: (value: string) => { - const label = STRUCTURE_LABELS[value] || value || '-'; - const properCase = label.split(/[_\s]+/).map(word => - word.charAt(0).toUpperCase() + word.slice(1) - ).join(' '); + const properCase = getStructureLabel(value) + .split(/[_\s]+/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); return ( - + {properCase} ); @@ -336,6 +343,23 @@ export function createApprovedPageConfig(params: { }, ]; + if (showSectorColumn) { + columns.splice(2, 0, { + key: 'sector_name', + label: 'Sector', + sortable: false, + width: '120px', + render: (value: string, row: Content) => { + const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors); + return ( + + {row.sector_name || '-'} + + ); + }, + }); + } + const filters: FilterConfig[] = [ { key: 'search', @@ -347,7 +371,7 @@ export function createApprovedPageConfig(params: { key: 'status', label: 'Status', type: 'select', - options: [ + options: params.statusOptions || [ { value: '', label: 'All' }, { value: 'draft', label: 'Draft' }, { value: 'review', label: 'Review' }, @@ -359,7 +383,7 @@ export function createApprovedPageConfig(params: { key: 'site_status', label: 'Site Status', type: 'select', - options: [ + options: params.siteStatusOptions || [ { value: '', label: 'All' }, { value: 'not_published', label: 'Not Published' }, { value: 'scheduled', label: 'Scheduled' }, @@ -372,7 +396,7 @@ export function createApprovedPageConfig(params: { key: 'content_type', label: 'Type', type: 'select', - options: [ + options: params.contentTypeOptions || [ { value: '', label: 'All Types' }, ...CONTENT_TYPE_OPTIONS, ], @@ -381,7 +405,7 @@ export function createApprovedPageConfig(params: { key: 'content_structure', label: 'Structure', type: 'select', - options: [ + options: params.contentStructureOptions || [ { value: '', label: 'All Structures' }, ...ALL_CONTENT_STRUCTURES, ], diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 7865b7db..0305aee5 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -16,6 +16,7 @@ import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { getDifficultyNumber } from '../../utils/difficulty'; import { Cluster } from '../../services/api'; +import { getSectorBadgeColor } from '../../utils/badgeColors'; import Input from '../../components/form/input/InputField'; import Label from '../../components/form/Label'; import Button from '../../components/ui/button/Button'; @@ -68,6 +69,7 @@ export interface ClustersPageConfig { export const createClustersPageConfig = ( handlers: { activeSector: { id: number; name: string } | null; + sectors?: Array<{ id: number; name: string }>; formData: { name: string; description?: string | null; @@ -121,7 +123,7 @@ export const createClustersPageConfig = ( ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Cluster) => ( - + {row.sector_name || '-'} ), diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index 85fe181c..421c1ee5 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { titleColumn, - statusColumn, createdWithActionsColumn, wordCountColumn, sectorColumn, @@ -14,7 +13,8 @@ import { import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { Content } from '../../services/api'; -import { CONTENT_TYPE_OPTIONS, STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping'; +import { CONTENT_TYPE_OPTIONS, TYPE_LABELS } from '../structureMapping'; +import { getSectorBadgeColor, getStructureBadgeColor, getStructureLabel } from '../../utils/badgeColors'; export interface ColumnConfig { key: string; @@ -77,14 +77,12 @@ const renderBadgeList = (items: string[], emptyLabel = '-') => { export const createContentPageConfig = ( handlers: { activeSector: { id: number; name: string } | null; + sectors?: Array<{ id: number; name: string }>; searchTerm: string; setSearchTerm: (value: string) => void; - statusFilter: string; - setStatusFilter: (value: string) => void; setCurrentPage: (page: number) => 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 }>; @@ -99,12 +97,6 @@ export const createContentPageConfig = ( ): ContentPageConfig => { const showSectorColumn = !handlers.activeSector; - const statusColors: Record = { - draft: 'warning', - review: 'info', - publish: 'success', - }; - return { columns: [ { @@ -133,7 +125,7 @@ export const createContentPageConfig = ( ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Content) => ( - + {row.sector_name || '-'} ), @@ -160,13 +152,13 @@ export const createContentPageConfig = ( sortable: true, sortField: 'content_structure', render: (value: string) => { - const label = STRUCTURE_LABELS[value] || value || '-'; + const label = getStructureLabel(value); // Proper case: capitalize first letter of each word const properCase = label.split(/[_\s]+/).map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); return ( - + {properCase} ); @@ -236,25 +228,6 @@ export const createContentPageConfig = ( ); }, }, - { - ...statusColumn, - sortable: true, - sortField: 'status', - render: (value: string) => { - const statusColors: Record = { - draft: 'amber', - published: 'success', - }; - const color = statusColors[value] || 'amber'; - // Proper case - const label = value ? value.charAt(0).toUpperCase() + value.slice(1) : 'Draft'; - return ( - - {label} - - ); - }, - }, // Removed the separate Status icon column. Both icons will be shown in the Created column below. { key: 'source', @@ -410,30 +383,16 @@ export const createContentPageConfig = ( type: 'text', placeholder: 'Search content...', }, - { - key: 'status', - label: 'Status', - type: 'select', - options: [ - { value: '', label: 'All Status' }, - ...(handlers.statusOptions !== undefined - ? handlers.statusOptions - : [ - { value: 'draft', label: 'Draft' }, - { value: 'review', label: 'Review' }, - { value: 'approved', label: 'Approved' }, - { value: 'published', label: 'Published' }, - ] - ), - ], - }, { key: 'content_type', label: 'Content Type', type: 'select', options: [ { value: '', label: 'All Types' }, - ...CONTENT_TYPE_OPTIONS, + ...(handlers.contentTypeOptions !== undefined + ? handlers.contentTypeOptions + : CONTENT_TYPE_OPTIONS + ), ], }, { @@ -442,20 +401,25 @@ export const createContentPageConfig = ( type: 'select', options: [ { value: '', label: 'All Structures' }, - { value: 'article', label: 'Article' }, - { value: 'guide', label: 'Guide' }, - { value: 'comparison', label: 'Comparison' }, - { value: 'review', label: 'Review' }, - { value: 'listicle', label: 'Listicle' }, - { value: 'landing_page', label: 'Landing Page' }, - { value: 'business_page', label: 'Business Page' }, - { value: 'service_page', label: 'Service Page' }, - { value: 'general', label: 'General' }, - { value: 'cluster_hub', label: 'Cluster Hub' }, - { value: 'product_page', label: 'Product Page' }, - { value: 'category_archive', label: 'Category Archive' }, - { value: 'tag_archive', label: 'Tag Archive' }, - { value: 'attribute_archive', label: 'Attribute Archive' }, + ...(handlers.contentStructureOptions !== undefined + ? handlers.contentStructureOptions + : [ + { value: 'article', label: 'Article' }, + { value: 'guide', label: 'Guide' }, + { value: 'comparison', label: 'Comparison' }, + { value: 'review', label: 'Review' }, + { value: 'listicle', label: 'Listicle' }, + { value: 'landing_page', label: 'Landing Page' }, + { value: 'business_page', label: 'Business Page' }, + { value: 'service_page', label: 'Service Page' }, + { value: 'general', label: 'General' }, + { value: 'cluster_hub', label: 'Cluster Hub' }, + { value: 'product_page', label: 'Product Page' }, + { value: 'category_archive', label: 'Category Archive' }, + { value: 'tag_archive', label: 'Tag Archive' }, + { value: 'attribute_archive', label: 'Attribute Archive' }, + ] + ), ], }, { diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index e95f8e7d..e21617d3 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -14,6 +14,7 @@ import { import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { ContentIdea, Cluster } from '../../services/api'; +import { getSectorBadgeColor, getStructureBadgeColor, getStructureLabel } from '../../utils/badgeColors'; export interface ColumnConfig { key: string; @@ -64,6 +65,7 @@ export const createIdeasPageConfig = ( handlers: { clusters: Array<{ id: number; name: string }>; activeSector: { id: number; name: string } | null; + sectors?: Array<{ id: number; name: string }>; formData: { idea_title: string; description?: string | null; @@ -116,7 +118,7 @@ export const createIdeasPageConfig = ( ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: ContentIdea) => ( - + {row.sector_name || '-'} ), @@ -127,12 +129,12 @@ export const createIdeasPageConfig = ( sortable: false, // Backend doesn't support sorting by content_structure sortField: 'content_structure', render: (value: string) => { - const label = value?.replace('_', ' ') || '-'; + const label = getStructureLabel(value); const properCase = label.split(/[_\s]+/).map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); return ( - + {properCase} ); diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index f4a368f3..1a80f4ed 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -49,6 +49,9 @@ export const createImagesPageConfig = ( setSearchTerm: (value: string) => void; statusFilter: string; setStatusFilter: (value: string) => void; + contentStatusFilter?: string; + setContentStatusFilter?: (value: string) => void; + contentStatusOptions?: Array<{ value: string; label: string }>; setCurrentPage: (page: number) => void; maxInArticleImages?: number; // Optional: max in-article images to display onGenerateImages?: (contentId: number) => void; // Handler for generate images button @@ -66,15 +69,17 @@ export const createImagesPageConfig = ( sortField: 'content_title', width: '400px', render: (_value: string, row: ContentImagesGroup) => { - const statusColors: Record = { + const statusColors: Record = { draft: 'warning', review: 'info', - publish: 'success', + approved: 'blue', + published: 'success', }; const statusLabels: Record = { draft: 'Draft', review: 'Review', - publish: 'Publish', + approved: 'Approved', + published: 'Published', }; return ( @@ -192,7 +197,7 @@ export const createImagesPageConfig = ( key: 'content_status', label: 'Content Status', type: 'select', - options: [ + options: handlers.contentStatusOptions || [ { value: '', label: 'All' }, { value: 'draft', label: 'Draft' }, { value: 'review', label: 'Review' }, diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index 6b3e7b10..de4bc0d8 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -22,6 +22,7 @@ import Badge from '../../components/ui/badge/Badge'; import { getDifficultyNumber, getDifficultyOptions, getDifficultyValueFromNumber } from '../../utils/difficulty'; import { formatRelativeDate } from '../../utils/date'; import { Keyword } from '../../services/api'; +import { getSectorBadgeColor } from '../../utils/badgeColors'; import Input from '../../components/form/input/InputField'; import Label from '../../components/form/Label'; import Button from '../../components/ui/button/Button'; @@ -102,6 +103,7 @@ export const createKeywordsPageConfig = ( handlers: { clusters: Array<{ id: number; name: string }>; activeSector: { id: number; name: string } | null; + sectors?: Array<{ id: number; name: string }>; formData: { keyword?: string; volume?: number | null; @@ -162,7 +164,7 @@ export const createKeywordsPageConfig = ( ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Keyword) => ( - + {row.sector_name || '-'} ), diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx index 243c06e6..2b66668e 100644 --- a/frontend/src/config/pages/review.config.tsx +++ b/frontend/src/config/pages/review.config.tsx @@ -6,8 +6,9 @@ import { Content } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; -import { CheckCircleIcon } from '../../icons'; -import { STRUCTURE_LABELS, TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping'; +import { TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping'; +import { getSectorBadgeColor, getStructureBadgeColor, getStructureLabel } from '../../utils/badgeColors'; +import type { Sector } from '../../store/sectorStore'; export interface ColumnConfig { key: string; @@ -48,13 +49,11 @@ export interface ReviewPageConfig { export function createReviewPageConfig(params: { searchTerm: string; setSearchTerm: (value: string) => void; - statusFilter: string; - setStatusFilter: (value: string) => void; setCurrentPage: (page: number) => void; activeSector: { id: number; name: string } | null; + sectors?: Sector[]; 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 }>; @@ -159,12 +158,12 @@ export function createReviewPageConfig(params: { sortable: true, sortField: 'content_structure', render: (value: string) => { - const label = STRUCTURE_LABELS[value] || value || '-'; - const properCase = label.split(/[_\s]+/).map(word => - word.charAt(0).toUpperCase() + word.slice(1) - ).join(' '); + const properCase = getStructureLabel(value) + .split(/[_\s]+/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); return ( - + {properCase} ); @@ -186,35 +185,6 @@ export function createReviewPageConfig(params: { ); }, }, - { - key: 'status', - label: 'Status', - sortable: true, - sortField: 'status', - render: (value: string, row: Content) => { - const status = value || 'draft'; - const statusColors: Record = { - draft: 'gray', - review: 'blue', - published: 'green', - scheduled: 'amber', - archived: 'red', - }; - const color = statusColors[status] || 'gray'; - const label = status.charAt(0).toUpperCase() + status.slice(1); - - return ( -
- - {label} - - {row.external_id && ( - - )} -
- ); - }, - }, { key: 'word_count', label: 'Words', @@ -251,11 +221,14 @@ export function createReviewPageConfig(params: { label: 'Sector', sortable: false, width: '120px', - render: (value: string, row: Content) => ( - - {row.sector_name || '-'} - - ), + render: (value: string, row: Content) => { + const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors); + return ( + + {row.sector_name || '-'} + + ); + }, }); } @@ -268,23 +241,11 @@ export function createReviewPageConfig(params: { type: 'text', placeholder: 'Search content...', }, - { - key: 'status', - label: 'Status', - type: 'select', - options: [ - { value: '', label: 'All' }, - { value: 'draft', label: 'Draft' }, - { value: 'review', label: 'Review' }, - { value: 'approved', label: 'Approved' }, - { value: 'published', label: 'Published' }, - ], - }, { key: 'site_status', label: 'Site Status', type: 'select', - options: [ + options: params.siteStatusOptions || [ { value: '', label: 'All' }, { value: 'not_published', label: 'Not Published' }, { value: 'scheduled', label: 'Scheduled' }, @@ -297,7 +258,7 @@ export function createReviewPageConfig(params: { key: 'content_type', label: 'Type', type: 'select', - options: [ + options: params.contentTypeOptions || [ { value: '', label: 'All Types' }, ...CONTENT_TYPE_OPTIONS, ], @@ -306,7 +267,7 @@ export function createReviewPageConfig(params: { key: 'content_structure', label: 'Structure', type: 'select', - options: [ + options: params.contentStructureOptions || [ { value: '', label: 'All Structures' }, ...ALL_CONTENT_STRUCTURES, ], diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 1a4b705c..dbf0ec4b 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -15,6 +15,7 @@ import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { Task, Cluster } from '../../services/api'; import { CONTENT_TYPE_OPTIONS, CONTENT_STRUCTURE_BY_TYPE, STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping'; +import { getSectorBadgeColor, getStructureBadgeColor } from '../../utils/badgeColors'; export interface ColumnConfig { key: string; @@ -68,6 +69,7 @@ export const createTasksPageConfig = ( handlers: { clusters: Array<{ id: number; name: string }>; activeSector: { id: number; name: string } | null; + sectors?: Array<{ id: number; name: string }>; formData: { title: string; description?: string | null; @@ -90,9 +92,12 @@ export const createTasksPageConfig = ( setStructureFilter: (value: string) => void; typeFilter: string; setTypeFilter: (value: string) => void; - sourceFilter: string; - setSourceFilter: (value: string) => void; setCurrentPage: (page: number) => void; + // Dynamic filter options + statusOptions?: Array<{ value: string; label: string }>; + contentTypeOptions?: Array<{ value: string; label: string }>; + contentStructureOptions?: Array<{ value: string; label: string }>; + clusterOptions?: Array<{ value: string; label: string }>; } ): TasksPageConfig => { const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors @@ -124,7 +129,7 @@ export const createTasksPageConfig = ( ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Task) => ( - + {row.sector_name || '-'} ), @@ -184,7 +189,7 @@ export const createTasksPageConfig = ( word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); return ( - + {properCase} ); @@ -314,8 +319,13 @@ export const createTasksPageConfig = ( type: 'select', options: [ { value: '', label: 'All Status' }, - { value: 'queued', label: 'Queued' }, - { value: 'completed', label: 'Completed' }, + ...(handlers.statusOptions !== undefined + ? handlers.statusOptions + : [ + { value: 'queued', label: 'Queued' }, + { value: 'completed', label: 'Completed' }, + ] + ), ], }, { @@ -324,7 +334,10 @@ export const createTasksPageConfig = ( type: 'select', options: [ { value: '', label: 'All Types' }, - ...CONTENT_TYPE_OPTIONS, + ...(handlers.contentTypeOptions !== undefined + ? handlers.contentTypeOptions + : CONTENT_TYPE_OPTIONS + ), ], }, { @@ -333,20 +346,25 @@ export const createTasksPageConfig = ( type: 'select', options: [ { value: '', label: 'All Structures' }, - { value: 'article', label: 'Article' }, - { value: 'guide', label: 'Guide' }, - { value: 'comparison', label: 'Comparison' }, - { value: 'review', label: 'Review' }, - { value: 'listicle', label: 'Listicle' }, - { value: 'landing_page', label: 'Landing Page' }, - { value: 'business_page', label: 'Business Page' }, - { value: 'service_page', label: 'Service Page' }, - { value: 'general', label: 'General' }, - { value: 'cluster_hub', label: 'Cluster Hub' }, - { value: 'product_page', label: 'Product Page' }, - { value: 'category_archive', label: 'Category Archive' }, - { value: 'tag_archive', label: 'Tag Archive' }, - { value: 'attribute_archive', label: 'Attribute Archive' }, + ...(handlers.contentStructureOptions !== undefined + ? handlers.contentStructureOptions + : [ + { value: 'article', label: 'Article' }, + { value: 'guide', label: 'Guide' }, + { value: 'comparison', label: 'Comparison' }, + { value: 'review', label: 'Review' }, + { value: 'listicle', label: 'Listicle' }, + { value: 'landing_page', label: 'Landing Page' }, + { value: 'business_page', label: 'Business Page' }, + { value: 'service_page', label: 'Service Page' }, + { value: 'general', label: 'General' }, + { value: 'cluster_hub', label: 'Cluster Hub' }, + { value: 'product_page', label: 'Product Page' }, + { value: 'category_archive', label: 'Category Archive' }, + { value: 'tag_archive', label: 'Tag Archive' }, + { value: 'attribute_archive', label: 'Attribute Archive' }, + ] + ), ], }, { @@ -356,7 +374,10 @@ export const createTasksPageConfig = ( options: (() => { return [ { value: '', label: 'All Clusters' }, - ...handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name })), + ...(handlers.clusterOptions !== undefined + ? handlers.clusterOptions + : handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name })) + ), ]; })(), dynamicOptions: 'clusters', diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index aaf84613..2f19fab3 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -36,7 +36,7 @@ import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeW export default function Clusters() { const toast = useToast(); const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); + const { activeSector, sectors } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state @@ -429,6 +429,7 @@ export default function Clusters() { const pageConfig = useMemo(() => { return createClustersPageConfig({ activeSector, + sectors, formData, setFormData, searchTerm, @@ -458,6 +459,7 @@ export default function Clusters() { }); }, [ activeSector, + sectors, formData, searchTerm, statusFilter, diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index 2f8548ca..ff137148 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -38,7 +38,7 @@ import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeW export default function Ideas() { const toast = useToast(); const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); + const { activeSector, sectors } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state @@ -341,6 +341,8 @@ export default function Ideas() { return createIdeasPageConfig({ clusters, activeSector, + sectors, + sectors, formData, setFormData, searchTerm, diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx index 57f8106b..c13ee9cd 100644 --- a/frontend/src/pages/Planner/Keywords.tsx +++ b/frontend/src/pages/Planner/Keywords.tsx @@ -42,7 +42,7 @@ import { createKeywordsPageConfig } from '../../config/pages/keywords.config'; export default function Keywords() { const toast = useToast(); const { activeSite } = useSiteStore(); - const { activeSector, loadSectorsForSite } = useSectorStore(); + const { activeSector, sectors, loadSectorsForSite } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state @@ -575,6 +575,7 @@ export default function Keywords() { return createKeywordsPageConfig({ clusters, activeSector, + sectors, formData, setFormData, // Filter state handlers @@ -611,6 +612,7 @@ export default function Keywords() { }, [ clusters, activeSector, + sectors, formData, searchTerm, statusFilter, diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx index bf9fcec3..8b32ee6b 100644 --- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx +++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx @@ -46,6 +46,7 @@ import { SectorMetricGrid, StatType } from '../../components/keywords-library/Se import SmartSuggestions from '../../components/keywords-library/SmartSuggestions'; import SectorCardsGrid from '../../components/keywords-library/SectorCardsGrid'; import BulkAddConfirmation from '../../components/keywords-library/BulkAddConfirmation'; +import { getSectorBadgeColor } from '../../utils/badgeColors'; export default function IndustriesSectorsKeywords() { const toast = useToast(); @@ -803,11 +804,14 @@ export default function IndustriesSectorsKeywords() { key: 'sector_name', label: 'Sector', sortable: false, - render: (_value: string, row: SeedKeyword) => ( - - {row.sector_name || '-'} - - ), + render: (_value: string, row: SeedKeyword) => { + const color = getSectorBadgeColor(row.sector_id, row.sector_name, sectors); + return ( + + {row.sector_name || '-'} + + ); + }, }] : []), { key: 'volume', diff --git a/frontend/src/pages/Writer/Approved.tsx b/frontend/src/pages/Writer/Approved.tsx index 8a726498..b062dfb0 100644 --- a/frontend/src/pages/Writer/Approved.tsx +++ b/frontend/src/pages/Writer/Approved.tsx @@ -37,7 +37,7 @@ import ErrorDetailsModal from '../../components/common/ErrorDetailsModal'; export default function Approved() { const toast = useToast(); const navigate = useNavigate(); - const { activeSector } = useSectorStore(); + const { activeSector, sectors } = useSectorStore(); const { activeSite } = useSiteStore(); const { pageSize } = usePageSizeStore(); @@ -670,11 +670,16 @@ export default function Approved() { setSiteStatusFilter, setCurrentPage, activeSector, + sectors, onRowClick: (row: Content) => { navigate(`/writer/content/${row.id}`); }, + statusOptions, + siteStatusOptions, + contentTypeOptions, + contentStructureOptions, }); - }, [searchTerm, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, activeSector, navigate]); + }, [searchTerm, statusFilter, siteStatusFilter, activeSector, sectors, statusOptions, siteStatusOptions, contentTypeOptions, contentStructureOptions, navigate]); // Calculate header metrics - use totals from API calls (not page data) // This ensures metrics show correct totals across all pages, not just current page diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index 1b0f4b83..8ad4f396 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -32,7 +32,7 @@ import { PencilSquareIcon } from '@heroicons/react/24/outline'; export default function Content() { const toast = useToast(); const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); + const { activeSector, sectors } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state @@ -48,14 +48,13 @@ export default function Content() { const [totalImagesCount, setTotalImagesCount] = useState(0); // Dynamic filter options (loaded from backend) - const [statusOptions, setStatusOptions] = useState | undefined>(undefined); const [sourceOptions, setSourceOptions] = useState | undefined>(undefined); const [contentTypeOptions, setContentTypeOptions] = useState | undefined>(undefined); const [contentStructureOptions, setContentStructureOptions] = useState | undefined>(undefined); // Filter state const [searchTerm, setSearchTerm] = useState(''); - const [statusFilter, setStatusFilter] = useState('draft'); + const [statusFilter] = useState('draft'); const [sourceFilter, setSourceFilter] = useState(''); const [contentTypeFilter, setContentTypeFilter] = useState(''); const [contentStructureFilter, setContentStructureFilter] = useState(''); @@ -90,7 +89,6 @@ export default function Content() { try { const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); - setStatusOptions(options.statuses || []); setSourceOptions(options.sources || []); setContentTypeOptions(options.content_types || []); setContentStructureOptions(options.content_structures || []); @@ -262,14 +260,12 @@ export default function Content() { const pageConfig = useMemo(() => { return createContentPageConfig({ activeSector, + sectors, searchTerm, setSearchTerm, - statusFilter, - setStatusFilter, setCurrentPage, onRowClick: handleRowClick, // Dynamic filter options - statusOptions, sourceOptions, contentTypeOptions, contentStructureOptions, @@ -283,10 +279,9 @@ export default function Content() { }); }, [ activeSector, + sectors, searchTerm, - statusFilter, handleRowClick, - statusOptions, sourceOptions, contentTypeOptions, contentStructureOptions, @@ -297,7 +292,6 @@ export default function Content() { sourceFilter, setSourceFilter, setSearchTerm, - setStatusFilter, setCurrentPage, ]); @@ -410,18 +404,22 @@ export default function Content() { filters={pageConfig.filters} filterValues={{ search: searchTerm, - status: statusFilter, source: sourceFilter, + content_type: contentTypeFilter, + content_structure: contentStructureFilter, }} onFilterChange={(key: string, value: any) => { if (key === 'search') { setSearchTerm(value); - } else if (key === 'status') { - setStatusFilter(value); - setCurrentPage(1); } else if (key === 'source') { setSourceFilter(value); setCurrentPage(1); + } else if (key === 'content_type') { + setContentTypeFilter(value); + setCurrentPage(1); + } else if (key === 'content_structure') { + setContentStructureFilter(value); + setCurrentPage(1); } }} pagination={{ diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 45e50929..446aa607 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -10,6 +10,7 @@ import { fetchContentImages, fetchImages, fetchContent, + fetchWriterContentFilterOptions, ContentImagesGroup, ContentImagesResponse, fetchImageGenerationSettings, @@ -55,6 +56,8 @@ export default function Images() { // Filter state const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); + const [contentStatusFilter, setContentStatusFilter] = useState(''); + const [contentStatusOptions, setContentStatusOptions] = useState | undefined>(undefined); const [selectedIds, setSelectedIds] = useState([]); // Pagination state (client-side for now) @@ -68,6 +71,32 @@ export default function Images() { const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [showContent, setShowContent] = useState(false); + // Load dynamic filter options for content status + const loadFilterOptions = useCallback(async (currentFilters?: { + status?: string; + search?: string; + }) => { + if (!activeSite) return; + + try { + const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); + setContentStatusOptions(options.statuses || []); + } catch (error) { + console.error('Error loading filter options:', error); + } + }, [activeSite]); + + useEffect(() => { + loadFilterOptions(); + }, [activeSite]); + + useEffect(() => { + loadFilterOptions({ + status: contentStatusFilter || undefined, + search: searchTerm || undefined, + }); + }, [contentStatusFilter, searchTerm, loadFilterOptions]); + // Image queue modal state const [isQueueModalOpen, setIsQueueModalOpen] = useState(false); const [imageQueue, setImageQueue] = useState([]); @@ -130,7 +159,14 @@ export default function Images() { ); } - // Client-side status filter + // Client-side content status filter + if (contentStatusFilter) { + filteredResults = filteredResults.filter(group => + group.content_status === contentStatusFilter + ); + } + + // Client-side image status filter if (statusFilter) { filteredResults = filteredResults.filter(group => group.overall_status === statusFilter @@ -479,12 +515,15 @@ export default function Images() { setSearchTerm, statusFilter, setStatusFilter, + contentStatusFilter, + setContentStatusFilter, + contentStatusOptions, setCurrentPage, maxInArticleImages, onGenerateImages: handleGenerateImages, onImageClick: handleImageClick, }); - }, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages, handleImageClick]); + }, [searchTerm, statusFilter, contentStatusFilter, contentStatusOptions, maxInArticleImages, handleGenerateImages, handleImageClick]); // Calculate header metrics - use totals from API calls (not page data) // This ensures metrics show correct totals across all pages, not just current page @@ -545,6 +584,7 @@ export default function Images() { filters={pageConfig.filters} filterValues={{ search: searchTerm, + content_status: contentStatusFilter, status: statusFilter, }} nextAction={selectedIds.length > 0 ? { @@ -560,6 +600,8 @@ export default function Images() { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { setSearchTerm(stringValue); + } else if (key === 'content_status') { + setContentStatusFilter(stringValue); } else if (key === 'status') { setStatusFilter(stringValue); } @@ -593,6 +635,7 @@ export default function Images() { headerMetrics={headerMetrics} onFilterReset={() => { setSearchTerm(''); + setContentStatusFilter(''); setStatusFilter(''); setCurrentPage(1); }} diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index bc0200e1..254751e6 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; -import { + const { activeSector, sectors } = useSectorStore(); fetchContent, fetchImages, fetchWriterContentFilterOptions, @@ -24,14 +24,12 @@ import { useSectorStore } from '../../store/sectorStore'; import { useSiteStore } from '../../store/siteStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; -import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter'; export default function Review() { const toast = useToast(); const navigate = useNavigate(); const { activeSector } = useSectorStore(); const { activeSite } = useSiteStore(); - const { pageSize } = usePageSizeStore(); // Data state const [content, setContent] = useState([]); @@ -46,21 +44,18 @@ export default function Review() { const [totalImagesCount, setTotalImagesCount] = useState(0); // Dynamic filter options (loaded from backend) - const [statusOptions, setStatusOptions] = useState | undefined>(undefined); const [siteStatusOptions, setSiteStatusOptions] = useState | undefined>(undefined); const [contentTypeOptions, setContentTypeOptions] = useState | undefined>(undefined); const [contentStructureOptions, setContentStructureOptions] = useState | undefined>(undefined); // Filter state - default to review status const [searchTerm, setSearchTerm] = useState(''); - const [statusFilter, setStatusFilter] = useState('review'); // Default to review const [siteStatusFilter, setSiteStatusFilter] = useState(''); const [contentTypeFilter, setContentTypeFilter] = useState(''); const [contentStructureFilter, setContentStructureFilter] = useState(''); const [selectedIds, setSelectedIds] = useState([]); // Pagination state - const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); @@ -71,7 +66,6 @@ export default function Review() { // 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; @@ -83,7 +77,6 @@ export default function Review() { try { const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); - setStatusOptions(options.statuses || []); setSiteStatusOptions(options.site_statuses || []); setContentTypeOptions(options.content_types || []); setContentStructureOptions(options.content_structures || []); @@ -94,7 +87,7 @@ export default function Review() { // Load filter options when site changes (initial load with no filters) useEffect(() => { - loadFilterOptions({ status: 'review' }); // Always pass review status + loadFilterOptions({ status: 'review' }); }, [activeSite]); // Reload filter options when any filter changes (cascading filters) @@ -189,8 +182,6 @@ export default function Review() { window.removeEventListener('sector-changed', handleSiteChange); }; }, [loadContent]); - - // Sorting handler const handleSort = useCallback((column: string) => { if (column === sortBy) { setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); @@ -210,14 +201,12 @@ export default function Review() { const pageConfig = useMemo(() => createReviewPageConfig({ activeSector, + sectors, searchTerm, setSearchTerm, - statusFilter, - setStatusFilter, setCurrentPage, onRowClick: handleRowClick, // Dynamic filter options - statusOptions, siteStatusOptions, contentTypeOptions, contentStructureOptions, @@ -231,10 +220,9 @@ export default function Review() { }), [ activeSector, + sectors, searchTerm, - statusFilter, handleRowClick, - statusOptions, siteStatusOptions, contentTypeOptions, contentStructureOptions, @@ -401,6 +389,9 @@ export default function Review() { filters={pageConfig.filters} filterValues={{ search: searchTerm, + site_status: siteStatusFilter, + content_type: contentTypeFilter, + content_structure: contentStructureFilter, }} primaryAction={{ label: 'Approve Selected', @@ -412,6 +403,12 @@ export default function Review() { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { setSearchTerm(stringValue); + } else if (key === 'site_status') { + setSiteStatusFilter(stringValue); + } else if (key === 'content_type') { + setContentTypeFilter(stringValue); + } else if (key === 'content_structure') { + setContentStructureFilter(stringValue); } setCurrentPage(1); }} @@ -442,6 +439,9 @@ export default function Review() { headerMetrics={headerMetrics} onFilterReset={() => { setSearchTerm(''); + setSiteStatusFilter(''); + setContentTypeFilter(''); + setContentStructureFilter(''); setCurrentPage(1); }} onRowAction={handleRowAction} diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index c7dc017b..85fd5e01 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -40,7 +40,7 @@ import { DocumentTextIcon } from '@heroicons/react/24/outline'; export default function Tasks() { const toast = useToast(); const { activeSite } = useSiteStore(); - const { activeSector } = useSectorStore(); + const { activeSector, sectors } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state @@ -66,7 +66,6 @@ export default function Tasks() { const [contentTypeOptions, setContentTypeOptions] = useState | undefined>(undefined); const [contentStructureOptions, setContentStructureOptions] = useState | undefined>(undefined); const [clusterOptions, setClusterOptions] = useState | undefined>(undefined); - const [sourceOptions, setSourceOptions] = useState | undefined>(undefined); // Filter state const [searchTerm, setSearchTerm] = useState(''); @@ -74,7 +73,6 @@ export default function Tasks() { const [clusterFilter, setClusterFilter] = useState(''); const [structureFilter, setStructureFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); - const [sourceFilter, setSourceFilter] = useState(''); const [selectedIds, setSelectedIds] = useState([]); // Pagination state @@ -115,7 +113,6 @@ export default function Tasks() { content_type?: string; content_structure?: string; cluster?: string; - source?: string; search?: string; }) => { if (!activeSite) return; @@ -126,7 +123,6 @@ export default function Tasks() { setContentTypeOptions(options.content_types || []); setContentStructureOptions(options.content_structures || []); setClusterOptions(options.clusters || []); - setSourceOptions(options.sources || []); } catch (error) { console.error('Error loading filter options:', error); } @@ -144,10 +140,9 @@ export default function Tasks() { content_type: typeFilter || undefined, content_structure: structureFilter || undefined, cluster: clusterFilter || undefined, - source: sourceFilter || undefined, search: searchTerm || undefined, }); - }, [statusFilter, typeFilter, structureFilter, clusterFilter, sourceFilter, searchTerm, loadFilterOptions]); + }, [statusFilter, typeFilter, structureFilter, clusterFilter, searchTerm, loadFilterOptions]); @@ -413,6 +408,7 @@ export default function Tasks() { return createTasksPageConfig({ clusters, activeSector, + sectors, formData, setFormData, searchTerm, @@ -420,16 +416,18 @@ export default function Tasks() { statusFilter, setStatusFilter, clusterFilter, - sourceFilter, - setSourceFilter, setClusterFilter, structureFilter, setStructureFilter, typeFilter, setTypeFilter, setCurrentPage, + statusOptions, + contentTypeOptions, + contentStructureOptions, + clusterOptions, }); - }, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter]); + }, [clusters, activeSector, sectors, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, statusOptions, contentTypeOptions, contentStructureOptions, clusterOptions]); // Calculate header metrics - use totals from API calls (not page data) // This ensures metrics show correct totals across all pages, not just current page @@ -524,7 +522,6 @@ export default function Tasks() { cluster_id: clusterFilter, content_structure: structureFilter, content_type: typeFilter, - source: sourceFilter, }} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); @@ -538,8 +535,6 @@ export default function Tasks() { setStructureFilter(stringValue); } else if (key === 'content_type') { setTypeFilter(stringValue); - } else if (key === 'source') { - setSourceFilter(stringValue); } setCurrentPage(1); }} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 5fdaf187..adcd10f2 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -856,7 +856,6 @@ export interface WriterTaskFilterOptions { content_types: FilterOption[]; content_structures: FilterOption[]; clusters: FilterOption[]; - sources: FilterOption[]; } export interface TaskFilterOptionsRequest { @@ -864,7 +863,6 @@ export interface TaskFilterOptionsRequest { content_type?: string; content_structure?: string; cluster?: string; - source?: string; search?: string; } @@ -878,7 +876,6 @@ export async function fetchWriterTaskFilterOptions( 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(); diff --git a/frontend/src/utils/badgeColors.ts b/frontend/src/utils/badgeColors.ts new file mode 100644 index 00000000..6287693d --- /dev/null +++ b/frontend/src/utils/badgeColors.ts @@ -0,0 +1,70 @@ +import { ALL_CONTENT_STRUCTURES, STRUCTURE_LABELS } from '../config/structureMapping'; +import type { Sector } from '../store/sectorStore'; + +const SECTOR_BADGE_PALETTE = ['brand', 'success', 'warning', 'info', 'purple'] as const; +const STRUCTURE_BADGE_PALETTE = ['brand', 'success', 'warning', 'info', 'purple'] as const; + +const normalize = (value?: string | null) => (value || '').trim().toLowerCase(); + +const hashString = (value: string) => { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash << 5) - hash + value.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash); +}; + +export const getSectorBadgeColor = ( + sectorId?: number | null, + sectorName?: string | null, + sectors?: Sector[] +): typeof SECTOR_BADGE_PALETTE[number] | 'neutral' => { + let resolvedId = sectorId ?? null; + + if (!resolvedId && sectorName && sectors && sectors.length > 0) { + const match = sectors.find((sector) => normalize(sector.name) === normalize(sectorName)); + if (match) { + resolvedId = match.id; + } + } + + const orderedIds = sectors && sectors.length > 0 + ? [...new Set(sectors.map((sector) => sector.id))].sort((a, b) => a - b) + : []; + + if (resolvedId && orderedIds.length > 0) { + const index = orderedIds.indexOf(resolvedId); + if (index >= 0) { + return SECTOR_BADGE_PALETTE[index % SECTOR_BADGE_PALETTE.length]; + } + } + + if (resolvedId) { + return SECTOR_BADGE_PALETTE[Math.abs(resolvedId) % SECTOR_BADGE_PALETTE.length]; + } + + if (sectorName) { + const index = hashString(sectorName) % SECTOR_BADGE_PALETTE.length; + return SECTOR_BADGE_PALETTE[index]; + } + + return 'neutral'; +}; + +export const getStructureBadgeColor = ( + structure?: string | null +): typeof STRUCTURE_BADGE_PALETTE[number] | 'neutral' => { + const structureKey = structure || ''; + const structureValues = ALL_CONTENT_STRUCTURES.map((item) => item.value); + const index = structureValues.indexOf(structureKey); + if (index >= 0) { + return STRUCTURE_BADGE_PALETTE[index % STRUCTURE_BADGE_PALETTE.length]; + } + return 'neutral'; +}; + +export const getStructureLabel = (structure?: string | null) => { + if (!structure) return '-'; + return STRUCTURE_LABELS[structure] || structure.replace(/_/g, ' '); +};