Filters and badges

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-19 09:25:34 +00:00
parent 29ce8139d9
commit 8c8f2df5dd
19 changed files with 322 additions and 223 deletions

View File

@@ -7,7 +7,9 @@ import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { CheckCircleIcon, ArrowRightIcon } from '../../icons'; 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 { export interface ColumnConfig {
key: string; key: string;
@@ -54,7 +56,12 @@ export function createApprovedPageConfig(params: {
setSiteStatusFilter: (value: string) => void; setSiteStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Sector[];
onRowClick?: (row: Content) => void; 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 { }): ApprovedPageConfig {
const showSectorColumn = !params.activeSector; const showSectorColumn = !params.activeSector;
@@ -230,12 +237,12 @@ export function createApprovedPageConfig(params: {
sortField: 'content_structure', sortField: 'content_structure',
width: '130px', width: '130px',
render: (value: string) => { render: (value: string) => {
const label = STRUCTURE_LABELS[value] || value || '-'; const properCase = getStructureLabel(value)
const properCase = label.split(/[_\s]+/).map(word => .split(/[_\s]+/)
word.charAt(0).toUpperCase() + word.slice(1) .map(word => word.charAt(0).toUpperCase() + word.slice(1))
).join(' '); .join(' ');
return ( return (
<Badge color="purple" size="xs" variant="soft"> <Badge color={getStructureBadgeColor(value)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span> <span className="text-[11px] font-normal">{properCase}</span>
</Badge> </Badge>
); );
@@ -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 (
<Badge color={color} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
</Badge>
);
},
});
}
const filters: FilterConfig[] = [ const filters: FilterConfig[] = [
{ {
key: 'search', key: 'search',
@@ -347,7 +371,7 @@ export function createApprovedPageConfig(params: {
key: 'status', key: 'status',
label: 'Status', label: 'Status',
type: 'select', type: 'select',
options: [ options: params.statusOptions || [
{ value: '', label: 'All' }, { value: '', label: 'All' },
{ value: 'draft', label: 'Draft' }, { value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' }, { value: 'review', label: 'Review' },
@@ -359,7 +383,7 @@ export function createApprovedPageConfig(params: {
key: 'site_status', key: 'site_status',
label: 'Site Status', label: 'Site Status',
type: 'select', type: 'select',
options: [ options: params.siteStatusOptions || [
{ value: '', label: 'All' }, { value: '', label: 'All' },
{ value: 'not_published', label: 'Not Published' }, { value: 'not_published', label: 'Not Published' },
{ value: 'scheduled', label: 'Scheduled' }, { value: 'scheduled', label: 'Scheduled' },
@@ -372,7 +396,7 @@ export function createApprovedPageConfig(params: {
key: 'content_type', key: 'content_type',
label: 'Type', label: 'Type',
type: 'select', type: 'select',
options: [ options: params.contentTypeOptions || [
{ value: '', label: 'All Types' }, { value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS, ...CONTENT_TYPE_OPTIONS,
], ],
@@ -381,7 +405,7 @@ export function createApprovedPageConfig(params: {
key: 'content_structure', key: 'content_structure',
label: 'Structure', label: 'Structure',
type: 'select', type: 'select',
options: [ options: params.contentStructureOptions || [
{ value: '', label: 'All Structures' }, { value: '', label: 'All Structures' },
...ALL_CONTENT_STRUCTURES, ...ALL_CONTENT_STRUCTURES,
], ],

View File

@@ -16,6 +16,7 @@ import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { getDifficultyNumber } from '../../utils/difficulty'; import { getDifficultyNumber } from '../../utils/difficulty';
import { Cluster } from '../../services/api'; import { Cluster } from '../../services/api';
import { getSectorBadgeColor } from '../../utils/badgeColors';
import Input from '../../components/form/input/InputField'; import Input from '../../components/form/input/InputField';
import Label from '../../components/form/Label'; import Label from '../../components/form/Label';
import Button from '../../components/ui/button/Button'; import Button from '../../components/ui/button/Button';
@@ -68,6 +69,7 @@ export interface ClustersPageConfig {
export const createClustersPageConfig = ( export const createClustersPageConfig = (
handlers: { handlers: {
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Array<{ id: number; name: string }>;
formData: { formData: {
name: string; name: string;
description?: string | null; description?: string | null;
@@ -121,7 +123,7 @@ export const createClustersPageConfig = (
...(showSectorColumn ? [{ ...(showSectorColumn ? [{
...sectorColumn, ...sectorColumn,
render: (value: string, row: Cluster) => ( render: (value: string, row: Cluster) => (
<Badge color="info" size="xs" variant="soft"> <Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span> <span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
</Badge> </Badge>
), ),

View File

@@ -6,7 +6,6 @@
import React from 'react'; import React from 'react';
import { import {
titleColumn, titleColumn,
statusColumn,
createdWithActionsColumn, createdWithActionsColumn,
wordCountColumn, wordCountColumn,
sectorColumn, sectorColumn,
@@ -14,7 +13,8 @@ import {
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { Content } from '../../services/api'; 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 { export interface ColumnConfig {
key: string; key: string;
@@ -77,14 +77,12 @@ const renderBadgeList = (items: string[], emptyLabel = '-') => {
export const createContentPageConfig = ( export const createContentPageConfig = (
handlers: { handlers: {
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Array<{ id: number; name: string }>;
searchTerm: string; searchTerm: string;
setSearchTerm: (value: string) => void; setSearchTerm: (value: string) => void;
statusFilter: string;
setStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
onRowClick?: (row: Content) => void; onRowClick?: (row: Content) => void;
// Dynamic filter options // Dynamic filter options
statusOptions?: Array<{ value: string; label: string }>;
sourceOptions?: Array<{ value: string; label: string }>; sourceOptions?: Array<{ value: string; label: string }>;
contentTypeOptions?: Array<{ value: string; label: string }>; contentTypeOptions?: Array<{ value: string; label: string }>;
contentStructureOptions?: Array<{ value: string; label: string }>; contentStructureOptions?: Array<{ value: string; label: string }>;
@@ -99,12 +97,6 @@ export const createContentPageConfig = (
): ContentPageConfig => { ): ContentPageConfig => {
const showSectorColumn = !handlers.activeSector; const showSectorColumn = !handlers.activeSector;
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
draft: 'warning',
review: 'info',
publish: 'success',
};
return { return {
columns: [ columns: [
{ {
@@ -133,7 +125,7 @@ export const createContentPageConfig = (
...(showSectorColumn ? [{ ...(showSectorColumn ? [{
...sectorColumn, ...sectorColumn,
render: (value: string, row: Content) => ( render: (value: string, row: Content) => (
<Badge color="info" size="xs" variant="soft"> <Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span> <span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
</Badge> </Badge>
), ),
@@ -160,13 +152,13 @@ export const createContentPageConfig = (
sortable: true, sortable: true,
sortField: 'content_structure', sortField: 'content_structure',
render: (value: string) => { render: (value: string) => {
const label = STRUCTURE_LABELS[value] || value || '-'; const label = getStructureLabel(value);
// Proper case: capitalize first letter of each word // Proper case: capitalize first letter of each word
const properCase = label.split(/[_\s]+/).map(word => const properCase = label.split(/[_\s]+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1) word.charAt(0).toUpperCase() + word.slice(1)
).join(' '); ).join(' ');
return ( return (
<Badge color="purple" size="xs" variant="soft"> <Badge color={getStructureBadgeColor(value)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span> <span className="text-[11px] font-normal">{properCase}</span>
</Badge> </Badge>
); );
@@ -236,25 +228,6 @@ export const createContentPageConfig = (
); );
}, },
}, },
{
...statusColumn,
sortable: true,
sortField: 'status',
render: (value: string) => {
const statusColors: Record<string, 'success' | 'amber'> = {
draft: 'amber',
published: 'success',
};
const color = statusColors[value] || 'amber';
// Proper case
const label = value ? value.charAt(0).toUpperCase() + value.slice(1) : 'Draft';
return (
<Badge color={color} size="xs" variant="soft">
<span className="text-[11px] font-normal">{label}</span>
</Badge>
);
},
},
// Removed the separate Status icon column. Both icons will be shown in the Created column below. // Removed the separate Status icon column. Both icons will be shown in the Created column below.
{ {
key: 'source', key: 'source',
@@ -410,30 +383,16 @@ export const createContentPageConfig = (
type: 'text', type: 'text',
placeholder: 'Search content...', 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', key: 'content_type',
label: 'Content Type', label: 'Content Type',
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Types' }, { value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS, ...(handlers.contentTypeOptions !== undefined
? handlers.contentTypeOptions
: CONTENT_TYPE_OPTIONS
),
], ],
}, },
{ {
@@ -442,6 +401,9 @@ export const createContentPageConfig = (
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Structures' }, { value: '', label: 'All Structures' },
...(handlers.contentStructureOptions !== undefined
? handlers.contentStructureOptions
: [
{ value: 'article', label: 'Article' }, { value: 'article', label: 'Article' },
{ value: 'guide', label: 'Guide' }, { value: 'guide', label: 'Guide' },
{ value: 'comparison', label: 'Comparison' }, { value: 'comparison', label: 'Comparison' },
@@ -456,6 +418,8 @@ export const createContentPageConfig = (
{ value: 'category_archive', label: 'Category Archive' }, { value: 'category_archive', label: 'Category Archive' },
{ value: 'tag_archive', label: 'Tag Archive' }, { value: 'tag_archive', label: 'Tag Archive' },
{ value: 'attribute_archive', label: 'Attribute Archive' }, { value: 'attribute_archive', label: 'Attribute Archive' },
]
),
], ],
}, },
{ {

View File

@@ -14,6 +14,7 @@ import {
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { ContentIdea, Cluster } from '../../services/api'; import { ContentIdea, Cluster } from '../../services/api';
import { getSectorBadgeColor, getStructureBadgeColor, getStructureLabel } from '../../utils/badgeColors';
export interface ColumnConfig { export interface ColumnConfig {
key: string; key: string;
@@ -64,6 +65,7 @@ export const createIdeasPageConfig = (
handlers: { handlers: {
clusters: Array<{ id: number; name: string }>; clusters: Array<{ id: number; name: string }>;
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Array<{ id: number; name: string }>;
formData: { formData: {
idea_title: string; idea_title: string;
description?: string | null; description?: string | null;
@@ -116,7 +118,7 @@ export const createIdeasPageConfig = (
...(showSectorColumn ? [{ ...(showSectorColumn ? [{
...sectorColumn, ...sectorColumn,
render: (value: string, row: ContentIdea) => ( render: (value: string, row: ContentIdea) => (
<Badge color="info" size="xs" variant="soft"> <Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span> <span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
</Badge> </Badge>
), ),
@@ -127,12 +129,12 @@ export const createIdeasPageConfig = (
sortable: false, // Backend doesn't support sorting by content_structure sortable: false, // Backend doesn't support sorting by content_structure
sortField: 'content_structure', sortField: 'content_structure',
render: (value: string) => { render: (value: string) => {
const label = value?.replace('_', ' ') || '-'; const label = getStructureLabel(value);
const properCase = label.split(/[_\s]+/).map(word => const properCase = label.split(/[_\s]+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1) word.charAt(0).toUpperCase() + word.slice(1)
).join(' '); ).join(' ');
return ( return (
<Badge color="purple" size="xs" variant="soft"> <Badge color={getStructureBadgeColor(value)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span> <span className="text-[11px] font-normal">{properCase}</span>
</Badge> </Badge>
); );

View File

@@ -49,6 +49,9 @@ export const createImagesPageConfig = (
setSearchTerm: (value: string) => void; setSearchTerm: (value: string) => void;
statusFilter: string; statusFilter: string;
setStatusFilter: (value: string) => void; setStatusFilter: (value: string) => void;
contentStatusFilter?: string;
setContentStatusFilter?: (value: string) => void;
contentStatusOptions?: Array<{ value: string; label: string }>;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
maxInArticleImages?: number; // Optional: max in-article images to display maxInArticleImages?: number; // Optional: max in-article images to display
onGenerateImages?: (contentId: number) => void; // Handler for generate images button onGenerateImages?: (contentId: number) => void; // Handler for generate images button
@@ -66,15 +69,17 @@ export const createImagesPageConfig = (
sortField: 'content_title', sortField: 'content_title',
width: '400px', width: '400px',
render: (_value: string, row: ContentImagesGroup) => { render: (_value: string, row: ContentImagesGroup) => {
const statusColors: Record<string, 'warning' | 'info' | 'success'> = { const statusColors: Record<string, 'warning' | 'info' | 'success' | 'blue' | 'gray'> = {
draft: 'warning', draft: 'warning',
review: 'info', review: 'info',
publish: 'success', approved: 'blue',
published: 'success',
}; };
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
draft: 'Draft', draft: 'Draft',
review: 'Review', review: 'Review',
publish: 'Publish', approved: 'Approved',
published: 'Published',
}; };
return ( return (
@@ -192,7 +197,7 @@ export const createImagesPageConfig = (
key: 'content_status', key: 'content_status',
label: 'Content Status', label: 'Content Status',
type: 'select', type: 'select',
options: [ options: handlers.contentStatusOptions || [
{ value: '', label: 'All' }, { value: '', label: 'All' },
{ value: 'draft', label: 'Draft' }, { value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' }, { value: 'review', label: 'Review' },

View File

@@ -22,6 +22,7 @@ import Badge from '../../components/ui/badge/Badge';
import { getDifficultyNumber, getDifficultyOptions, getDifficultyValueFromNumber } from '../../utils/difficulty'; import { getDifficultyNumber, getDifficultyOptions, getDifficultyValueFromNumber } from '../../utils/difficulty';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { Keyword } from '../../services/api'; import { Keyword } from '../../services/api';
import { getSectorBadgeColor } from '../../utils/badgeColors';
import Input from '../../components/form/input/InputField'; import Input from '../../components/form/input/InputField';
import Label from '../../components/form/Label'; import Label from '../../components/form/Label';
import Button from '../../components/ui/button/Button'; import Button from '../../components/ui/button/Button';
@@ -102,6 +103,7 @@ export const createKeywordsPageConfig = (
handlers: { handlers: {
clusters: Array<{ id: number; name: string }>; clusters: Array<{ id: number; name: string }>;
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Array<{ id: number; name: string }>;
formData: { formData: {
keyword?: string; keyword?: string;
volume?: number | null; volume?: number | null;
@@ -162,7 +164,7 @@ export const createKeywordsPageConfig = (
...(showSectorColumn ? [{ ...(showSectorColumn ? [{
...sectorColumn, ...sectorColumn,
render: (value: string, row: Keyword) => ( render: (value: string, row: Keyword) => (
<Badge color="info" size="xs" variant="soft"> <Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft">
{row.sector_name || '-'} {row.sector_name || '-'}
</Badge> </Badge>
), ),

View File

@@ -6,8 +6,9 @@
import { Content } from '../../services/api'; import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { CheckCircleIcon } from '../../icons'; import { TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping';
import { STRUCTURE_LABELS, 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 { export interface ColumnConfig {
key: string; key: string;
@@ -48,13 +49,11 @@ export interface ReviewPageConfig {
export function createReviewPageConfig(params: { export function createReviewPageConfig(params: {
searchTerm: string; searchTerm: string;
setSearchTerm: (value: string) => void; setSearchTerm: (value: string) => void;
statusFilter: string;
setStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void; setCurrentPage: (page: number) => void;
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Sector[];
onRowClick?: (row: Content) => void; onRowClick?: (row: Content) => void;
// Dynamic filter options // Dynamic filter options
statusOptions?: Array<{ value: string; label: string }>;
siteStatusOptions?: Array<{ value: string; label: string }>; siteStatusOptions?: Array<{ value: string; label: string }>;
contentTypeOptions?: Array<{ value: string; label: string }>; contentTypeOptions?: Array<{ value: string; label: string }>;
contentStructureOptions?: Array<{ value: string; label: string }>; contentStructureOptions?: Array<{ value: string; label: string }>;
@@ -159,12 +158,12 @@ export function createReviewPageConfig(params: {
sortable: true, sortable: true,
sortField: 'content_structure', sortField: 'content_structure',
render: (value: string) => { render: (value: string) => {
const label = STRUCTURE_LABELS[value] || value || '-'; const properCase = getStructureLabel(value)
const properCase = label.split(/[_\s]+/).map(word => .split(/[_\s]+/)
word.charAt(0).toUpperCase() + word.slice(1) .map(word => word.charAt(0).toUpperCase() + word.slice(1))
).join(' '); .join(' ');
return ( return (
<Badge color="purple" size="xs" variant="soft"> <Badge color={getStructureBadgeColor(value)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span> <span className="text-[11px] font-normal">{properCase}</span>
</Badge> </Badge>
); );
@@ -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<string, 'gray' | 'blue' | 'green' | 'amber' | 'red'> = {
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 (
<div className="flex items-center gap-1.5">
<Badge color={color} size="xs" variant="soft">
<span className="text-[11px] font-normal">{label}</span>
</Badge>
{row.external_id && (
<CheckCircleIcon className="w-3.5 h-3.5 text-success-500" title="Published to Site" />
)}
</div>
);
},
},
{ {
key: 'word_count', key: 'word_count',
label: 'Words', label: 'Words',
@@ -251,11 +221,14 @@ export function createReviewPageConfig(params: {
label: 'Sector', label: 'Sector',
sortable: false, sortable: false,
width: '120px', width: '120px',
render: (value: string, row: Content) => ( render: (value: string, row: Content) => {
<Badge color="info" size="xs" variant="soft"> const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors);
return (
<Badge color={color} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span> <span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
</Badge> </Badge>
), );
},
}); });
} }
@@ -268,23 +241,11 @@ export function createReviewPageConfig(params: {
type: 'text', type: 'text',
placeholder: 'Search content...', 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', key: 'site_status',
label: 'Site Status', label: 'Site Status',
type: 'select', type: 'select',
options: [ options: params.siteStatusOptions || [
{ value: '', label: 'All' }, { value: '', label: 'All' },
{ value: 'not_published', label: 'Not Published' }, { value: 'not_published', label: 'Not Published' },
{ value: 'scheduled', label: 'Scheduled' }, { value: 'scheduled', label: 'Scheduled' },
@@ -297,7 +258,7 @@ export function createReviewPageConfig(params: {
key: 'content_type', key: 'content_type',
label: 'Type', label: 'Type',
type: 'select', type: 'select',
options: [ options: params.contentTypeOptions || [
{ value: '', label: 'All Types' }, { value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS, ...CONTENT_TYPE_OPTIONS,
], ],
@@ -306,7 +267,7 @@ export function createReviewPageConfig(params: {
key: 'content_structure', key: 'content_structure',
label: 'Structure', label: 'Structure',
type: 'select', type: 'select',
options: [ options: params.contentStructureOptions || [
{ value: '', label: 'All Structures' }, { value: '', label: 'All Structures' },
...ALL_CONTENT_STRUCTURES, ...ALL_CONTENT_STRUCTURES,
], ],

View File

@@ -15,6 +15,7 @@ import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
import { Task, Cluster } from '../../services/api'; import { Task, Cluster } from '../../services/api';
import { CONTENT_TYPE_OPTIONS, CONTENT_STRUCTURE_BY_TYPE, STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping'; import { CONTENT_TYPE_OPTIONS, CONTENT_STRUCTURE_BY_TYPE, STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
import { getSectorBadgeColor, getStructureBadgeColor } from '../../utils/badgeColors';
export interface ColumnConfig { export interface ColumnConfig {
key: string; key: string;
@@ -68,6 +69,7 @@ export const createTasksPageConfig = (
handlers: { handlers: {
clusters: Array<{ id: number; name: string }>; clusters: Array<{ id: number; name: string }>;
activeSector: { id: number; name: string } | null; activeSector: { id: number; name: string } | null;
sectors?: Array<{ id: number; name: string }>;
formData: { formData: {
title: string; title: string;
description?: string | null; description?: string | null;
@@ -90,9 +92,12 @@ export const createTasksPageConfig = (
setStructureFilter: (value: string) => void; setStructureFilter: (value: string) => void;
typeFilter: string; typeFilter: string;
setTypeFilter: (value: string) => void; setTypeFilter: (value: string) => void;
sourceFilter: string;
setSourceFilter: (value: string) => void;
setCurrentPage: (page: number) => 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 => { ): TasksPageConfig => {
const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors
@@ -124,7 +129,7 @@ export const createTasksPageConfig = (
...(showSectorColumn ? [{ ...(showSectorColumn ? [{
...sectorColumn, ...sectorColumn,
render: (value: string, row: Task) => ( render: (value: string, row: Task) => (
<Badge color="info" size="xs" variant="soft"> <Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span> <span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
</Badge> </Badge>
), ),
@@ -184,7 +189,7 @@ export const createTasksPageConfig = (
word.charAt(0).toUpperCase() + word.slice(1) word.charAt(0).toUpperCase() + word.slice(1)
).join(' '); ).join(' ');
return ( return (
<Badge color="purple" size="xs" variant="soft"> <Badge color={getStructureBadgeColor(value)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{properCase}</span> <span className="text-[11px] font-normal">{properCase}</span>
</Badge> </Badge>
); );
@@ -314,8 +319,13 @@ export const createTasksPageConfig = (
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Status' }, { value: '', label: 'All Status' },
...(handlers.statusOptions !== undefined
? handlers.statusOptions
: [
{ value: 'queued', label: 'Queued' }, { value: 'queued', label: 'Queued' },
{ value: 'completed', label: 'Completed' }, { value: 'completed', label: 'Completed' },
]
),
], ],
}, },
{ {
@@ -324,7 +334,10 @@ export const createTasksPageConfig = (
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Types' }, { value: '', label: 'All Types' },
...CONTENT_TYPE_OPTIONS, ...(handlers.contentTypeOptions !== undefined
? handlers.contentTypeOptions
: CONTENT_TYPE_OPTIONS
),
], ],
}, },
{ {
@@ -333,6 +346,9 @@ export const createTasksPageConfig = (
type: 'select', type: 'select',
options: [ options: [
{ value: '', label: 'All Structures' }, { value: '', label: 'All Structures' },
...(handlers.contentStructureOptions !== undefined
? handlers.contentStructureOptions
: [
{ value: 'article', label: 'Article' }, { value: 'article', label: 'Article' },
{ value: 'guide', label: 'Guide' }, { value: 'guide', label: 'Guide' },
{ value: 'comparison', label: 'Comparison' }, { value: 'comparison', label: 'Comparison' },
@@ -347,6 +363,8 @@ export const createTasksPageConfig = (
{ value: 'category_archive', label: 'Category Archive' }, { value: 'category_archive', label: 'Category Archive' },
{ value: 'tag_archive', label: 'Tag Archive' }, { value: 'tag_archive', label: 'Tag Archive' },
{ value: 'attribute_archive', label: 'Attribute Archive' }, { value: 'attribute_archive', label: 'Attribute Archive' },
]
),
], ],
}, },
{ {
@@ -356,7 +374,10 @@ export const createTasksPageConfig = (
options: (() => { options: (() => {
return [ return [
{ value: '', label: 'All Clusters' }, { 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', dynamicOptions: 'clusters',

View File

@@ -36,7 +36,7 @@ import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeW
export default function Clusters() { export default function Clusters() {
const toast = useToast(); const toast = useToast();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore(); const { activeSector, sectors } = useSectorStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
@@ -429,6 +429,7 @@ export default function Clusters() {
const pageConfig = useMemo(() => { const pageConfig = useMemo(() => {
return createClustersPageConfig({ return createClustersPageConfig({
activeSector, activeSector,
sectors,
formData, formData,
setFormData, setFormData,
searchTerm, searchTerm,
@@ -458,6 +459,7 @@ export default function Clusters() {
}); });
}, [ }, [
activeSector, activeSector,
sectors,
formData, formData,
searchTerm, searchTerm,
statusFilter, statusFilter,

View File

@@ -38,7 +38,7 @@ import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeW
export default function Ideas() { export default function Ideas() {
const toast = useToast(); const toast = useToast();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore(); const { activeSector, sectors } = useSectorStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
@@ -341,6 +341,8 @@ export default function Ideas() {
return createIdeasPageConfig({ return createIdeasPageConfig({
clusters, clusters,
activeSector, activeSector,
sectors,
sectors,
formData, formData,
setFormData, setFormData,
searchTerm, searchTerm,

View File

@@ -42,7 +42,7 @@ import { createKeywordsPageConfig } from '../../config/pages/keywords.config';
export default function Keywords() { export default function Keywords() {
const toast = useToast(); const toast = useToast();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { activeSector, loadSectorsForSite } = useSectorStore(); const { activeSector, sectors, loadSectorsForSite } = useSectorStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
@@ -575,6 +575,7 @@ export default function Keywords() {
return createKeywordsPageConfig({ return createKeywordsPageConfig({
clusters, clusters,
activeSector, activeSector,
sectors,
formData, formData,
setFormData, setFormData,
// Filter state handlers // Filter state handlers
@@ -611,6 +612,7 @@ export default function Keywords() {
}, [ }, [
clusters, clusters,
activeSector, activeSector,
sectors,
formData, formData,
searchTerm, searchTerm,
statusFilter, statusFilter,

View File

@@ -46,6 +46,7 @@ import { SectorMetricGrid, StatType } from '../../components/keywords-library/Se
import SmartSuggestions from '../../components/keywords-library/SmartSuggestions'; import SmartSuggestions from '../../components/keywords-library/SmartSuggestions';
import SectorCardsGrid from '../../components/keywords-library/SectorCardsGrid'; import SectorCardsGrid from '../../components/keywords-library/SectorCardsGrid';
import BulkAddConfirmation from '../../components/keywords-library/BulkAddConfirmation'; import BulkAddConfirmation from '../../components/keywords-library/BulkAddConfirmation';
import { getSectorBadgeColor } from '../../utils/badgeColors';
export default function IndustriesSectorsKeywords() { export default function IndustriesSectorsKeywords() {
const toast = useToast(); const toast = useToast();
@@ -803,11 +804,14 @@ export default function IndustriesSectorsKeywords() {
key: 'sector_name', key: 'sector_name',
label: 'Sector', label: 'Sector',
sortable: false, sortable: false,
render: (_value: string, row: SeedKeyword) => ( render: (_value: string, row: SeedKeyword) => {
<Badge color="info" size="sm" variant="light"> const color = getSectorBadgeColor(row.sector_id, row.sector_name, sectors);
return (
<Badge color={color} size="sm" variant="light">
{row.sector_name || '-'} {row.sector_name || '-'}
</Badge> </Badge>
), );
},
}] : []), }] : []),
{ {
key: 'volume', key: 'volume',

View File

@@ -37,7 +37,7 @@ import ErrorDetailsModal from '../../components/common/ErrorDetailsModal';
export default function Approved() { export default function Approved() {
const toast = useToast(); const toast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { activeSector } = useSectorStore(); const { activeSector, sectors } = useSectorStore();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
@@ -670,11 +670,16 @@ export default function Approved() {
setSiteStatusFilter, setSiteStatusFilter,
setCurrentPage, setCurrentPage,
activeSector, activeSector,
sectors,
onRowClick: (row: Content) => { onRowClick: (row: Content) => {
navigate(`/writer/content/${row.id}`); 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) // 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

View File

@@ -32,7 +32,7 @@ import { PencilSquareIcon } from '@heroicons/react/24/outline';
export default function Content() { export default function Content() {
const toast = useToast(); const toast = useToast();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore(); const { activeSector, sectors } = useSectorStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
@@ -48,14 +48,13 @@ export default function Content() {
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
// Dynamic filter options (loaded from backend) // 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 [sourceOptions, setSourceOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
const [contentTypeOptions, setContentTypeOptions] = 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 [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] = useState('draft');
const [sourceFilter, setSourceFilter] = useState(''); const [sourceFilter, setSourceFilter] = useState('');
const [contentTypeFilter, setContentTypeFilter] = useState(''); const [contentTypeFilter, setContentTypeFilter] = useState('');
const [contentStructureFilter, setContentStructureFilter] = useState(''); const [contentStructureFilter, setContentStructureFilter] = useState('');
@@ -90,7 +89,6 @@ export default function Content() {
try { try {
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
setStatusOptions(options.statuses || []);
setSourceOptions(options.sources || []); setSourceOptions(options.sources || []);
setContentTypeOptions(options.content_types || []); setContentTypeOptions(options.content_types || []);
setContentStructureOptions(options.content_structures || []); setContentStructureOptions(options.content_structures || []);
@@ -262,14 +260,12 @@ export default function Content() {
const pageConfig = useMemo(() => { const pageConfig = useMemo(() => {
return createContentPageConfig({ return createContentPageConfig({
activeSector, activeSector,
sectors,
searchTerm, searchTerm,
setSearchTerm, setSearchTerm,
statusFilter,
setStatusFilter,
setCurrentPage, setCurrentPage,
onRowClick: handleRowClick, onRowClick: handleRowClick,
// Dynamic filter options // Dynamic filter options
statusOptions,
sourceOptions, sourceOptions,
contentTypeOptions, contentTypeOptions,
contentStructureOptions, contentStructureOptions,
@@ -283,10 +279,9 @@ export default function Content() {
}); });
}, [ }, [
activeSector, activeSector,
sectors,
searchTerm, searchTerm,
statusFilter,
handleRowClick, handleRowClick,
statusOptions,
sourceOptions, sourceOptions,
contentTypeOptions, contentTypeOptions,
contentStructureOptions, contentStructureOptions,
@@ -297,7 +292,6 @@ export default function Content() {
sourceFilter, sourceFilter,
setSourceFilter, setSourceFilter,
setSearchTerm, setSearchTerm,
setStatusFilter,
setCurrentPage, setCurrentPage,
]); ]);
@@ -410,18 +404,22 @@ export default function Content() {
filters={pageConfig.filters} filters={pageConfig.filters}
filterValues={{ filterValues={{
search: searchTerm, search: searchTerm,
status: statusFilter,
source: sourceFilter, source: sourceFilter,
content_type: contentTypeFilter,
content_structure: contentStructureFilter,
}} }}
onFilterChange={(key: string, value: any) => { onFilterChange={(key: string, value: any) => {
if (key === 'search') { if (key === 'search') {
setSearchTerm(value); setSearchTerm(value);
} else if (key === 'status') {
setStatusFilter(value);
setCurrentPage(1);
} else if (key === 'source') { } else if (key === 'source') {
setSourceFilter(value); setSourceFilter(value);
setCurrentPage(1); setCurrentPage(1);
} else if (key === 'content_type') {
setContentTypeFilter(value);
setCurrentPage(1);
} else if (key === 'content_structure') {
setContentStructureFilter(value);
setCurrentPage(1);
} }
}} }}
pagination={{ pagination={{

View File

@@ -10,6 +10,7 @@ import {
fetchContentImages, fetchContentImages,
fetchImages, fetchImages,
fetchContent, fetchContent,
fetchWriterContentFilterOptions,
ContentImagesGroup, ContentImagesGroup,
ContentImagesResponse, ContentImagesResponse,
fetchImageGenerationSettings, fetchImageGenerationSettings,
@@ -55,6 +56,8 @@ export default function Images() {
// Filter state // Filter state
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState(''); const [statusFilter, setStatusFilter] = useState('');
const [contentStatusFilter, setContentStatusFilter] = useState('');
const [contentStatusOptions, setContentStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state (client-side for now) // Pagination state (client-side for now)
@@ -68,6 +71,32 @@ export default function Images() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [showContent, setShowContent] = useState(false); 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 // Image queue modal state
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false); const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]); const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
@@ -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) { if (statusFilter) {
filteredResults = filteredResults.filter(group => filteredResults = filteredResults.filter(group =>
group.overall_status === statusFilter group.overall_status === statusFilter
@@ -479,12 +515,15 @@ export default function Images() {
setSearchTerm, setSearchTerm,
statusFilter, statusFilter,
setStatusFilter, setStatusFilter,
contentStatusFilter,
setContentStatusFilter,
contentStatusOptions,
setCurrentPage, setCurrentPage,
maxInArticleImages, maxInArticleImages,
onGenerateImages: handleGenerateImages, onGenerateImages: handleGenerateImages,
onImageClick: handleImageClick, 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) // 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
@@ -545,6 +584,7 @@ export default function Images() {
filters={pageConfig.filters} filters={pageConfig.filters}
filterValues={{ filterValues={{
search: searchTerm, search: searchTerm,
content_status: contentStatusFilter,
status: statusFilter, status: statusFilter,
}} }}
nextAction={selectedIds.length > 0 ? { nextAction={selectedIds.length > 0 ? {
@@ -560,6 +600,8 @@ export default function Images() {
const stringValue = value === null || value === undefined ? '' : String(value); const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') { if (key === 'search') {
setSearchTerm(stringValue); setSearchTerm(stringValue);
} else if (key === 'content_status') {
setContentStatusFilter(stringValue);
} else if (key === 'status') { } else if (key === 'status') {
setStatusFilter(stringValue); setStatusFilter(stringValue);
} }
@@ -593,6 +635,7 @@ export default function Images() {
headerMetrics={headerMetrics} headerMetrics={headerMetrics}
onFilterReset={() => { onFilterReset={() => {
setSearchTerm(''); setSearchTerm('');
setContentStatusFilter('');
setStatusFilter(''); setStatusFilter('');
setCurrentPage(1); setCurrentPage(1);
}} }}

View File

@@ -7,7 +7,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate'; import TablePageTemplate from '../../templates/TablePageTemplate';
import { const { activeSector, sectors } = useSectorStore();
fetchContent, fetchContent,
fetchImages, fetchImages,
fetchWriterContentFilterOptions, fetchWriterContentFilterOptions,
@@ -24,14 +24,12 @@ 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 PageHeader from '../../components/common/PageHeader'; import PageHeader from '../../components/common/PageHeader';
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
export default function Review() { export default function Review() {
const toast = useToast(); const toast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { activeSector } = useSectorStore(); const { activeSector } = useSectorStore();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { pageSize } = usePageSizeStore();
// Data state // Data state
const [content, setContent] = useState<Content[]>([]); const [content, setContent] = useState<Content[]>([]);
@@ -46,21 +44,18 @@ export default function Review() {
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
// Dynamic filter options (loaded from backend) // 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 [siteStatusOptions, setSiteStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
const [contentTypeOptions, setContentTypeOptions] = 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 [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 [siteStatusFilter, setSiteStatusFilter] = useState(''); const [siteStatusFilter, setSiteStatusFilter] = useState('');
const [contentTypeFilter, setContentTypeFilter] = useState(''); const [contentTypeFilter, setContentTypeFilter] = useState('');
const [contentStructureFilter, setContentStructureFilter] = useState(''); const [contentStructureFilter, setContentStructureFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state // Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0); 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 // 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 // This implements cascading filters - each filter's options reflect what's available
// given the other currently applied filters
const loadFilterOptions = useCallback(async (currentFilters?: { const loadFilterOptions = useCallback(async (currentFilters?: {
status?: string; status?: string;
site_status?: string; site_status?: string;
@@ -83,7 +77,6 @@ export default function Review() {
try { try {
const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters);
setStatusOptions(options.statuses || []);
setSiteStatusOptions(options.site_statuses || []); setSiteStatusOptions(options.site_statuses || []);
setContentTypeOptions(options.content_types || []); setContentTypeOptions(options.content_types || []);
setContentStructureOptions(options.content_structures || []); setContentStructureOptions(options.content_structures || []);
@@ -94,7 +87,7 @@ export default function Review() {
// Load filter options when site changes (initial load with no filters) // Load filter options when site changes (initial load with no filters)
useEffect(() => { useEffect(() => {
loadFilterOptions({ status: 'review' }); // Always pass review status loadFilterOptions({ status: 'review' });
}, [activeSite]); }, [activeSite]);
// Reload filter options when any filter changes (cascading filters) // Reload filter options when any filter changes (cascading filters)
@@ -189,8 +182,6 @@ export default function Review() {
window.removeEventListener('sector-changed', handleSiteChange); window.removeEventListener('sector-changed', handleSiteChange);
}; };
}, [loadContent]); }, [loadContent]);
// Sorting handler
const handleSort = useCallback((column: string) => { const handleSort = useCallback((column: string) => {
if (column === sortBy) { if (column === sortBy) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
@@ -210,14 +201,12 @@ export default function Review() {
const pageConfig = useMemo(() => const pageConfig = useMemo(() =>
createReviewPageConfig({ createReviewPageConfig({
activeSector, activeSector,
sectors,
searchTerm, searchTerm,
setSearchTerm, setSearchTerm,
statusFilter,
setStatusFilter,
setCurrentPage, setCurrentPage,
onRowClick: handleRowClick, onRowClick: handleRowClick,
// Dynamic filter options // Dynamic filter options
statusOptions,
siteStatusOptions, siteStatusOptions,
contentTypeOptions, contentTypeOptions,
contentStructureOptions, contentStructureOptions,
@@ -231,10 +220,9 @@ export default function Review() {
}), }),
[ [
activeSector, activeSector,
sectors,
searchTerm, searchTerm,
statusFilter,
handleRowClick, handleRowClick,
statusOptions,
siteStatusOptions, siteStatusOptions,
contentTypeOptions, contentTypeOptions,
contentStructureOptions, contentStructureOptions,
@@ -401,6 +389,9 @@ export default function Review() {
filters={pageConfig.filters} filters={pageConfig.filters}
filterValues={{ filterValues={{
search: searchTerm, search: searchTerm,
site_status: siteStatusFilter,
content_type: contentTypeFilter,
content_structure: contentStructureFilter,
}} }}
primaryAction={{ primaryAction={{
label: 'Approve Selected', label: 'Approve Selected',
@@ -412,6 +403,12 @@ export default function Review() {
const stringValue = value === null || value === undefined ? '' : String(value); const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') { if (key === 'search') {
setSearchTerm(stringValue); 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); setCurrentPage(1);
}} }}
@@ -442,6 +439,9 @@ export default function Review() {
headerMetrics={headerMetrics} headerMetrics={headerMetrics}
onFilterReset={() => { onFilterReset={() => {
setSearchTerm(''); setSearchTerm('');
setSiteStatusFilter('');
setContentTypeFilter('');
setContentStructureFilter('');
setCurrentPage(1); setCurrentPage(1);
}} }}
onRowAction={handleRowAction} onRowAction={handleRowAction}

View File

@@ -40,7 +40,7 @@ import { DocumentTextIcon } from '@heroicons/react/24/outline';
export default function Tasks() { export default function Tasks() {
const toast = useToast(); const toast = useToast();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore(); const { activeSector, sectors } = useSectorStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
@@ -66,7 +66,6 @@ export default function Tasks() {
const [contentTypeOptions, setContentTypeOptions] = 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 [contentStructureOptions, setContentStructureOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
const [clusterOptions, setClusterOptions] = 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('');
@@ -74,7 +73,6 @@ export default function Tasks() {
const [clusterFilter, setClusterFilter] = useState(''); const [clusterFilter, setClusterFilter] = useState('');
const [structureFilter, setStructureFilter] = useState(''); const [structureFilter, setStructureFilter] = useState('');
const [typeFilter, setTypeFilter] = useState(''); const [typeFilter, setTypeFilter] = useState('');
const [sourceFilter, setSourceFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state // Pagination state
@@ -115,7 +113,6 @@ export default function Tasks() {
content_type?: string; content_type?: string;
content_structure?: string; content_structure?: string;
cluster?: string; cluster?: string;
source?: string;
search?: string; search?: string;
}) => { }) => {
if (!activeSite) return; if (!activeSite) return;
@@ -126,7 +123,6 @@ export default function Tasks() {
setContentTypeOptions(options.content_types || []); setContentTypeOptions(options.content_types || []);
setContentStructureOptions(options.content_structures || []); setContentStructureOptions(options.content_structures || []);
setClusterOptions(options.clusters || []); setClusterOptions(options.clusters || []);
setSourceOptions(options.sources || []);
} catch (error) { } catch (error) {
console.error('Error loading filter options:', error); console.error('Error loading filter options:', error);
} }
@@ -144,10 +140,9 @@ export default function Tasks() {
content_type: typeFilter || undefined, content_type: typeFilter || undefined,
content_structure: structureFilter || undefined, content_structure: structureFilter || undefined,
cluster: clusterFilter || undefined, cluster: clusterFilter || undefined,
source: sourceFilter || undefined,
search: searchTerm || 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({ return createTasksPageConfig({
clusters, clusters,
activeSector, activeSector,
sectors,
formData, formData,
setFormData, setFormData,
searchTerm, searchTerm,
@@ -420,16 +416,18 @@ export default function Tasks() {
statusFilter, statusFilter,
setStatusFilter, setStatusFilter,
clusterFilter, clusterFilter,
sourceFilter,
setSourceFilter,
setClusterFilter, setClusterFilter,
structureFilter, structureFilter,
setStructureFilter, setStructureFilter,
typeFilter, typeFilter,
setTypeFilter, setTypeFilter,
setCurrentPage, 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) // 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
@@ -524,7 +522,6 @@ export default function Tasks() {
cluster_id: clusterFilter, cluster_id: clusterFilter,
content_structure: structureFilter, content_structure: structureFilter,
content_type: typeFilter, content_type: typeFilter,
source: sourceFilter,
}} }}
onFilterChange={(key, value) => { onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value); const stringValue = value === null || value === undefined ? '' : String(value);
@@ -538,8 +535,6 @@ export default function Tasks() {
setStructureFilter(stringValue); setStructureFilter(stringValue);
} else if (key === 'content_type') { } else if (key === 'content_type') {
setTypeFilter(stringValue); setTypeFilter(stringValue);
} else if (key === 'source') {
setSourceFilter(stringValue);
} }
setCurrentPage(1); setCurrentPage(1);
}} }}

View File

@@ -856,7 +856,6 @@ export interface WriterTaskFilterOptions {
content_types: FilterOption[]; content_types: FilterOption[];
content_structures: FilterOption[]; content_structures: FilterOption[];
clusters: FilterOption[]; clusters: FilterOption[];
sources: FilterOption[];
} }
export interface TaskFilterOptionsRequest { export interface TaskFilterOptionsRequest {
@@ -864,7 +863,6 @@ export interface TaskFilterOptionsRequest {
content_type?: string; content_type?: string;
content_structure?: string; content_structure?: string;
cluster?: string; cluster?: string;
source?: string;
search?: 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_type) params.append('content_type', filters.content_type);
if (filters?.content_structure) params.append('content_structure', filters.content_structure); if (filters?.content_structure) params.append('content_structure', filters.content_structure);
if (filters?.cluster) params.append('cluster', filters.cluster); if (filters?.cluster) params.append('cluster', filters.cluster);
if (filters?.source) params.append('source', filters.source);
if (filters?.search) params.append('search', filters.search); if (filters?.search) params.append('search', filters.search);
const queryString = params.toString(); const queryString = params.toString();

View File

@@ -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, ' ');
};