Enhance Content Management: Add sector name to ContentSerializer, improve Content view with pagination and search filters, and refactor Content page for better data handling and display.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-11 15:55:32 +00:00
parent 0924a8436c
commit 618ed0543d
9 changed files with 498 additions and 228 deletions

View File

@@ -144,6 +144,7 @@ class ImagesSerializer(serializers.ModelSerializer):
class ContentSerializer(serializers.ModelSerializer): class ContentSerializer(serializers.ModelSerializer):
"""Serializer for Content model""" """Serializer for Content model"""
task_title = serializers.SerializerMethodField() task_title = serializers.SerializerMethodField()
sector_name = serializers.SerializerMethodField()
class Meta: class Meta:
model = Content model = Content
@@ -151,6 +152,7 @@ class ContentSerializer(serializers.ModelSerializer):
'id', 'id',
'task_id', 'task_id',
'task_title', 'task_title',
'sector_name',
'html_content', 'html_content',
'word_count', 'word_count',
'metadata', 'metadata',
@@ -178,3 +180,14 @@ class ContentSerializer(serializers.ModelSerializer):
return None return None
return None return None
def get_sector_name(self, obj):
"""Get sector name from Sector model"""
if obj.sector_id:
try:
from igny8_core.auth.models import Sector
sector = Sector.objects.get(id=obj.sector_id)
return sector.name
except Sector.DoesNotExist:
return None
return None

View File

@@ -440,11 +440,13 @@ class ContentViewSet(SiteSectorModelViewSet):
""" """
queryset = Content.objects.all() queryset = Content.objects.all()
serializer_class = ContentSerializer serializer_class = ContentSerializer
pagination_class = CustomPageNumberPagination
filter_backends = [DjangoFilterBackend, filters.OrderingFilter] filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
ordering_fields = ['generated_at', 'updated_at'] search_fields = ['title', 'meta_title', 'primary_keyword']
ordering_fields = ['generated_at', 'updated_at', 'word_count', 'status']
ordering = ['-generated_at'] ordering = ['-generated_at']
filterset_fields = ['task_id'] filterset_fields = ['task_id', 'status']
def perform_create(self, serializer): def perform_create(self, serializer):
"""Override to automatically set account""" """Override to automatically set account"""

View File

@@ -24,6 +24,7 @@ const KeywordOpportunities = lazy(() => import("./pages/Planner/KeywordOpportuni
// Writer Module - Lazy loaded // Writer Module - Lazy loaded
const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard")); const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard"));
const Tasks = lazy(() => import("./pages/Writer/Tasks")); const Tasks = lazy(() => import("./pages/Writer/Tasks"));
const Content = lazy(() => import("./pages/Writer/Content"));
const Drafts = lazy(() => import("./pages/Writer/Drafts")); const Drafts = lazy(() => import("./pages/Writer/Drafts"));
const Images = lazy(() => import("./pages/Writer/Images")); const Images = lazy(() => import("./pages/Writer/Images"));
const Published = lazy(() => import("./pages/Writer/Published")); const Published = lazy(() => import("./pages/Writer/Published"));
@@ -160,7 +161,7 @@ export default function App() {
} /> } />
<Route path="/writer/content" element={ <Route path="/writer/content" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<Drafts /> <Content />
</Suspense> </Suspense>
} /> } />
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} /> <Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />

View File

@@ -109,7 +109,13 @@ const ToggleTableRow: React.FC<ToggleTableRowProps> = ({
> >
<div <div
ref={contentRef} ref={contentRef}
className="overflow-hidden" className="overflow-hidden bg-white dark:bg-gray-900"
style={{
maxWidth: '1200px',
margin: 'auto',
padding: '25px',
marginTop: '50px',
}}
> >
<div className="py-4 px-2"> <div className="py-4 px-2">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@@ -117,6 +123,14 @@ const ToggleTableRow: React.FC<ToggleTableRowProps> = ({
{contentLabel} {contentLabel}
</div> </div>
{/* Show idea title if available (for Tasks page) */}
{row.idea_title && (
<div className="mb-2">
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-1">Idea:</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{row.idea_title}</div>
</div>
)}
{/* Metadata badges */} {/* Metadata badges */}
<ToggleMetadata row={row} contentMetadata={contentMetadata} /> <ToggleMetadata row={row} contentMetadata={contentMetadata} />

View File

@@ -0,0 +1,247 @@
/**
* Content Page Configuration
* Centralized config for Content page table, filters, and actions
*/
import React from 'react';
import {
titleColumn,
statusColumn,
createdColumn,
wordCountColumn,
sectorColumn,
} from '../snippets/columns.snippets';
import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date';
import { Content } from '../../services/api';
export interface ColumnConfig {
key: string;
label: string;
sortable?: boolean;
sortField?: string;
align?: 'left' | 'center' | 'right';
width?: string;
render?: (value: any, row: any) => React.ReactNode;
toggleable?: boolean;
toggleContentKey?: string;
toggleContentLabel?: string;
}
export interface FilterConfig {
key: string;
label: string;
type: 'text' | 'select';
placeholder?: string;
options?: Array<{ value: string; label: string }>;
}
export interface HeaderMetricConfig {
label: string;
value: number;
accentColor: 'blue' | 'green' | 'amber' | 'purple';
calculate: (data: { content: any[]; totalCount: number }) => number;
}
export interface ContentPageConfig {
columns: ColumnConfig[];
filters: FilterConfig[];
headerMetrics: HeaderMetricConfig[];
}
const getList = (primary?: string[], fallback?: any): string[] => {
if (primary && primary.length > 0) return primary;
if (!fallback) return [];
if (Array.isArray(fallback)) return fallback;
return [];
};
const renderBadgeList = (items: string[], emptyLabel = '-') => {
if (!items || items.length === 0) {
return <span className="text-gray-400 dark:text-gray-500">{emptyLabel}</span>;
}
return (
<div className="flex flex-wrap gap-1">
{items.map((item, index) => (
<Badge key={`${item}-${index}`} color="light" size="sm" variant="light">
{item}
</Badge>
))}
</div>
);
};
export const createContentPageConfig = (
handlers: {
activeSector: { id: number; name: string } | null;
searchTerm: string;
setSearchTerm: (value: string) => void;
statusFilter: string;
setStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void;
}
): ContentPageConfig => {
const showSectorColumn = !handlers.activeSector;
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
draft: 'warning',
review: 'info',
publish: 'success',
};
return {
columns: [
{
...titleColumn,
sortable: true,
sortField: 'title',
toggleable: true,
toggleContentKey: 'html_content',
toggleContentLabel: 'Generated Content',
render: (value: string, row: Content) => (
<div>
<div className="font-medium text-gray-900 dark:text-white">
{row.meta_title || row.title || row.task_title || `Task #${row.task}`}
</div>
{row.meta_description && (
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{row.meta_description}
</div>
)}
</div>
),
},
...(showSectorColumn ? [{
...sectorColumn,
render: (value: string, row: Content) => (
<Badge color="info" size="sm" variant="light">
{row.sector_name || '-'}
</Badge>
),
}] : []),
{
key: 'primary_keyword',
label: 'Primary Keyword',
sortable: false,
width: '150px',
render: (value: string, row: Content) => (
row.primary_keyword ? (
<Badge color="info" size="sm" variant="light">
{row.primary_keyword}
</Badge>
) : (
<span className="text-gray-400 dark:text-gray-500">-</span>
)
),
},
{
key: 'secondary_keywords',
label: 'Secondary Keywords',
sortable: false,
width: '200px',
render: (_value: any, row: Content) => {
const secondaryKeywords = getList(
row.secondary_keywords,
row.metadata?.secondary_keywords
);
return renderBadgeList(secondaryKeywords);
},
},
{
key: 'tags',
label: 'Tags',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const tags = getList(row.tags, row.metadata?.tags);
return renderBadgeList(tags);
},
},
{
key: 'categories',
label: 'Categories',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const categories = getList(row.categories, row.metadata?.categories);
return renderBadgeList(categories);
},
},
{
...wordCountColumn,
sortable: true,
sortField: 'word_count',
render: (value: number) => value?.toLocaleString() ?? '-',
},
{
...statusColumn,
sortable: true,
sortField: 'status',
render: (value: string) => {
const status = value || 'draft';
const color = statusColors[status] || 'primary';
const label = status.replace('_', ' ').replace(/^\w/, (c) => c.toUpperCase());
return (
<Badge color={color} size="sm" variant="light">
{label}
</Badge>
);
},
},
{
...createdColumn,
sortable: true,
sortField: 'generated_at',
label: 'Generated',
render: (value: string) => formatRelativeDate(value),
},
],
filters: [
{
key: 'search',
label: 'Search',
type: 'text',
placeholder: 'Search content...',
},
{
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: '', label: 'All Status' },
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Review' },
{ value: 'publish', label: 'Publish' },
],
},
],
headerMetrics: [
{
label: 'Total Content',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
},
{
label: 'Draft',
value: 0,
accentColor: 'warning' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length,
},
{
label: 'Review',
value: 0,
accentColor: 'info' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length,
},
{
label: 'Published',
value: 0,
accentColor: 'success' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'publish').length,
},
],
};
};

View File

@@ -100,8 +100,8 @@ export const createTasksPageConfig = (
sortable: true, sortable: true,
sortField: 'title', sortField: 'title',
toggleable: true, toggleable: true,
toggleContentKey: 'content_html', toggleContentKey: 'description',
toggleContentLabel: 'Generated Content', toggleContentLabel: 'Idea & Content Outline',
}, },
// Sector column - only show when viewing all sectors // Sector column - only show when viewing all sectors
...(showSectorColumn ? [{ ...(showSectorColumn ? [{

View File

@@ -1,232 +1,191 @@
import { useState, useEffect } from 'react'; /**
import PageMeta from '../../components/common/PageMeta'; * Content Page - Built with TablePageTemplate
import { useToast } from '../../components/ui/toast/ToastContainer'; * Displays content from Content table with filters and pagination
import { fetchContent, Content as ContentType } from '../../services/api'; */
import Badge from '../../components/ui/badge/Badge';
import HTMLContentRenderer from '../../components/common/HTMLContentRenderer';
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = { import { useState, useEffect, useMemo, useCallback } from 'react';
draft: 'warning', import TablePageTemplate from '../../templates/TablePageTemplate';
review: 'info', import {
publish: 'success', fetchContent,
}; Content as ContentType,
ContentFilters,
} from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon } from '../../icons';
import { createContentPageConfig } from '../../config/pages/content.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
export default function Content() { export default function Content() {
const toast = useToast(); const toast = useToast();
const { activeSector } = useSectorStore();
const { pageSize } = usePageSizeStore();
// Data state
const [content, setContent] = useState<ContentType[]>([]); const [content, setContent] = useState<ContentType[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expandedId, setExpandedId] = useState<number | null>(null);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// Sorting state
const [sortBy, setSortBy] = useState<string>('generated_at');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// Load content - wrapped in useCallback
const loadContent = useCallback(async () => {
setLoading(true);
setShowContent(false);
try {
const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-generated_at';
const filters: ContentFilters = {
...(searchTerm && { search: searchTerm }),
...(statusFilter && { status: statusFilter }),
page: currentPage,
page_size: pageSize,
ordering,
};
const data = await fetchContent(filters);
setContent(data.results || []);
setTotalCount(data.count || 0);
setTotalPages(Math.ceil((data.count || 0) / pageSize));
setTimeout(() => {
setShowContent(true);
setLoading(false);
}, 100);
} catch (error: any) {
console.error('Error loading content:', error);
toast.error(`Failed to load content: ${error.message}`);
setShowContent(true);
setLoading(false);
}
}, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize, toast]);
useEffect(() => { useEffect(() => {
loadContent(); loadContent();
}, []); }, [loadContent]);
const loadContent = async () => { // Listen for site and sector changes and refresh data
try { useEffect(() => {
setLoading(true); const handleSiteChange = () => {
const response = await fetchContent(); loadContent();
setContent(response.results || []);
} catch (error: any) {
toast.error(`Failed to load content: ${error.message}`);
} finally {
setLoading(false);
}
}; };
const formatDate = (value: string) => const handleSectorChange = () => {
new Date(value).toLocaleString(undefined, { loadContent();
year: 'numeric', };
month: 'short',
day: 'numeric', window.addEventListener('siteChanged', handleSiteChange);
hour: 'numeric', window.addEventListener('sectorChanged', handleSectorChange);
minute: 'numeric', return () => {
window.removeEventListener('siteChanged', handleSiteChange);
window.removeEventListener('sectorChanged', handleSectorChange);
};
}, [loadContent]);
// Reset to page 1 when pageSize changes
useEffect(() => {
setCurrentPage(1);
}, [pageSize]);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadContent();
} else {
setCurrentPage(1);
}
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadContent]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
setSortBy(field || 'generated_at');
setSortDirection(direction);
setCurrentPage(1);
};
// Create page config
const pageConfig = useMemo(() => {
return createContentPageConfig({
activeSector,
searchTerm,
setSearchTerm,
statusFilter,
setStatusFilter,
setCurrentPage,
}); });
}, [
activeSector,
searchTerm,
statusFilter,
]);
const getList = (primary?: string[], fallback?: any): string[] => { // Calculate header metrics
if (primary && primary.length > 0) return primary; const headerMetrics = useMemo(() => {
if (!fallback) return []; if (!pageConfig?.headerMetrics) return [];
if (Array.isArray(fallback)) return fallback; return pageConfig.headerMetrics.map((metric) => ({
return []; label: metric.label,
}; value: metric.calculate({ content, totalCount }),
accentColor: metric.accentColor,
const renderBadgeList = (items: string[], emptyLabel = '-') => { }));
if (!items || items.length === 0) { }, [pageConfig?.headerMetrics, content, totalCount]);
return <span className="text-gray-400 dark:text-gray-500">{emptyLabel}</span>;
}
return ( return (
<div className="flex flex-wrap gap-1"> <>
{items.map((item, index) => ( <TablePageTemplate
<Badge key={`${item}-${index}`} color="light" size="sm" variant="light"> title="Content"
{item} titleIcon={<FileIcon className="text-brand-500 size-5" />}
</Badge> subtitle="Review AI-generated content and metadata"
))} columns={pageConfig.columns}
</div> data={content}
); loading={loading}
}; showContent={showContent}
filters={pageConfig.filters}
return ( filterValues={{
<div className="p-6"> search: searchTerm,
<PageMeta title="Content" /> status: statusFilter,
<div className="mb-6"> }}
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Content</h1> onFilterChange={(key: string, value: any) => {
<p className="text-gray-600 dark:text-gray-400 mt-1">Review AI-generated drafts and metadata</p> if (key === 'search') {
</div> setSearchTerm(value);
} else if (key === 'status') {
{loading ? ( setStatusFilter(value);
<div className="flex items-center justify-center h-64"> setCurrentPage(1);
<div className="text-gray-500 dark:text-gray-400">Loading...</div>
</div>
) : content.length === 0 ? (
<div className="rounded-lg border border-dashed border-gray-300 dark:border-gray-700 p-12 text-center text-gray-500 dark:text-gray-400">
No content generated yet. Run an AI content job to see drafts here.
</div>
) : (
<div className="overflow-x-auto rounded-xl border border-gray-200 dark:border-white/[0.05] bg-white dark:bg-gray-900">
<table className="min-w-full divide-y divide-gray-200 dark:divide-white/[0.05]">
<thead className="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Title
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Primary Keyword
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Secondary Keywords
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Tags
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Categories
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Word Count
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Status
</th>
<th className="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Generated
</th>
<th className="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Content
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-white/[0.05]">
{content.map((item) => {
const isExpanded = expandedId === item.id;
const secondaryKeywords = getList(
item.secondary_keywords,
item.metadata?.secondary_keywords
);
const tags = getList(item.tags, item.metadata?.tags);
const categories = getList(item.categories, item.metadata?.categories);
return (
<tr key={item.id} className="bg-white dark:bg-gray-900">
<td className="px-5 py-4 align-top">
<div className="font-medium text-gray-900 dark:text-white">
{item.meta_title || item.title || item.task_title || `Task #${item.task}`}
</div>
{(() => {
let metaDesc = item.meta_description;
// If meta_description is a JSON string, extract the actual value
if (metaDesc && typeof metaDesc === 'string' && metaDesc.trim().startsWith('{')) {
try {
const parsed = JSON.parse(metaDesc);
metaDesc = parsed.meta_description || metaDesc;
} catch {
// Not valid JSON, use as-is
} }
} }}
return metaDesc ? ( pagination={{
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2"> currentPage,
{metaDesc} totalPages,
</div> totalCount,
) : null; onPageChange: setCurrentPage,
})()} }}
</td> sorting={{
<td className="px-5 py-4 align-top"> sortBy,
{item.primary_keyword ? ( sortDirection,
<Badge color="info" size="sm" variant="light"> onSort: handleSort,
{item.primary_keyword} }}
</Badge> selection={{
) : ( selectedIds,
<span className="text-gray-400 dark:text-gray-500">-</span> onSelectionChange: setSelectedIds,
)} }}
</td> headerMetrics={headerMetrics}
<td className="px-5 py-4 align-top"> getItemDisplayName={(row: ContentType) => row.meta_title || row.title || `Content #${row.id}`}
{renderBadgeList(secondaryKeywords)}
</td>
<td className="px-5 py-4 align-top">
{renderBadgeList(tags)}
</td>
<td className="px-5 py-4 align-top">
{renderBadgeList(categories)}
</td>
<td className="px-5 py-4 align-top text-gray-700 dark:text-gray-300">
{item.word_count?.toLocaleString?.() ?? '-'}
</td>
<td className="px-5 py-4 align-top">
<Badge
color={statusColors[item.status] || 'primary'}
size="sm"
variant="light"
>
{(item.status || 'draft').replace('_', ' ').replace(/^\w/, (c) => c.toUpperCase())}
</Badge>
</td>
<td className="px-5 py-4 align-top text-gray-600 dark:text-gray-400">
{formatDate(item.generated_at)}
</td>
<td className="px-5 py-4 align-top text-right">
<button
onClick={() => setExpandedId(isExpanded ? null : item.id)}
className="text-sm font-medium text-blue-light-500 hover:text-blue-light-600 dark:text-blue-light-400 dark:hover:text-blue-light-300"
>
{isExpanded ? 'Hide' : 'View'}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{content.map((item) =>
expandedId === item.id ? (
<div
key={`expanded-${item.id}`}
className="mt-6 rounded-xl border border-gray-200 dark:border-white/[0.05] bg-white dark:bg-gray-900 p-6"
>
<div className="mb-4 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{item.meta_title || item.title || `Task #${item.task}`}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Generated {formatDate(item.generated_at)} Task #{item.task}
</p>
</div>
<button
onClick={() => setExpandedId(null)}
className="text-sm font-medium text-blue-light-500 hover:text-blue-light-600 dark:text-blue-light-400 dark:hover:text-blue-light-300"
>
Close
</button>
</div>
<HTMLContentRenderer
content={item.html_content}
className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed"
/> />
</div> </>
) : null
)}
</div>
); );
} }

View File

@@ -548,7 +548,7 @@ export default function Tasks() {
return ( return (
<> <>
<TablePageTemplate <TablePageTemplate
title="Content Tasks" title="Tasks"
titleIcon={<TaskIcon className="text-brand-500 size-5" />} titleIcon={<TaskIcon className="text-brand-500 size-5" />}
subtitle="Manage content generation queue and tasks" subtitle="Manage content generation queue and tasks"
columns={pageConfig.columns} columns={pageConfig.columns}

View File

@@ -1451,10 +1451,22 @@ export async function deleteAuthorProfile(id: number): Promise<void> {
} }
// Content API // Content API
export interface ContentFilters {
search?: string;
status?: string;
task_id?: number;
page?: number;
page_size?: number;
ordering?: string;
site_id?: number;
sector_id?: number;
}
export interface Content { export interface Content {
id: number; id: number;
task: number; task: number;
task_title?: string | null; task_title?: string | null;
sector_name?: string | null;
title?: string | null; title?: string | null;
meta_title?: string | null; meta_title?: string | null;
meta_description?: string | null; meta_description?: string | null;
@@ -1477,13 +1489,35 @@ export interface ContentResponse {
results: Content[]; results: Content[];
} }
export async function fetchContent(filters?: { export async function fetchContent(filters: ContentFilters = {}): Promise<ContentResponse> {
task_id?: number;
page?: number;
}): Promise<ContentResponse> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filters?.task_id) params.append('task_id', filters.task_id.toString());
if (filters?.page) params.append('page', filters.page.toString()); // Automatically add active site filter if not explicitly provided
if (!filters.site_id) {
const activeSiteId = getActiveSiteId();
if (activeSiteId) {
filters.site_id = activeSiteId;
}
}
// Automatically add active sector filter if not explicitly provided
if (filters.sector_id === undefined) {
const activeSectorId = getActiveSectorId();
if (activeSectorId !== null && activeSectorId !== undefined) {
filters.sector_id = activeSectorId;
}
}
if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status);
if (filters.task_id) params.append('task_id', filters.task_id.toString());
if (filters.site_id) params.append('site_id', filters.site_id.toString());
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
if (filters.page) params.append('page', filters.page.toString());
if (filters.page_size !== undefined && filters.page_size !== null) {
params.append('page_size', filters.page_size.toString());
}
if (filters.ordering) params.append('ordering', filters.ordering);
const queryString = params.toString(); const queryString = params.toString();
return fetchAPI(`/v1/writer/content/${queryString ? `?${queryString}` : ''}`); return fetchAPI(`/v1/writer/content/${queryString ? `?${queryString}` : ''}`);