Section 1 & 2 - #Migration Run

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 06:29:13 +00:00
parent dd63403e94
commit f81fffc9a6
17 changed files with 149 additions and 44 deletions

View File

@@ -1472,13 +1472,13 @@ class AutomationService:
time.sleep(delay) time.sleep(delay)
def run_stage_7(self): def run_stage_7(self):
"""Stage 7: Auto-Approve and Publish Review Content """Stage 7: Auto-Approve Review Content
This stage automatically approves content in 'review' status and This stage automatically approves content in 'review' status and
marks it as 'published' (or queues for WordPress sync). marks it as 'approved' (ready for publishing to WordPress).
""" """
stage_number = 7 stage_number = 7
stage_name = "Review → Published" stage_name = "Review → Approved"
start_time = time.time() start_time = time.time()
# Query content ready for review # Query content ready for review
@@ -1538,7 +1538,7 @@ class AutomationService:
'review_total': total_count, 'review_total': total_count,
'approved_count': approved_count, 'approved_count': approved_count,
'content_ids': list(Content.objects.filter( 'content_ids': list(Content.objects.filter(
site=self.site, status='published', updated_at__gte=self.run.started_at site=self.site, status='approved', updated_at__gte=self.run.started_at
).values_list('id', flat=True)), ).values_list('id', flat=True)),
'partial': True, 'partial': True,
'stopped_reason': reason, 'stopped_reason': reason,
@@ -1553,8 +1553,8 @@ class AutomationService:
stage_number, f"Approving content {idx}/{total_count}: {content.title}" stage_number, f"Approving content {idx}/{total_count}: {content.title}"
) )
# Approve content by changing status to 'published' # Approve content by changing status to 'approved' (ready for publishing)
content.status = 'published' content.status = 'approved'
content.save(update_fields=['status', 'updated_at']) content.save(update_fields=['status', 'updated_at'])
approved_count += 1 approved_count += 1
@@ -1593,7 +1593,7 @@ class AutomationService:
time_elapsed = self._format_time_elapsed(start_time) time_elapsed = self._format_time_elapsed(start_time)
content_ids = list(Content.objects.filter( content_ids = list(Content.objects.filter(
site=self.site, site=self.site,
status='published', status='approved',
updated_at__gte=self.run.started_at updated_at__gte=self.run.started_at
).values_list('id', flat=True)) ).values_list('id', flat=True))
@@ -1617,7 +1617,7 @@ class AutomationService:
# Release lock # Release lock
cache.delete(f'automation_lock_{self.site.id}') cache.delete(f'automation_lock_{self.site.id}')
logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved and published") logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved (ready for publishing)")
def pause_automation(self): def pause_automation(self):
"""Pause current automation run""" """Pause current automation run"""

View File

@@ -271,7 +271,8 @@ class Content(SoftDeletableModel, SiteSectorBaseModel):
STATUS_CHOICES = [ STATUS_CHOICES = [
('draft', 'Draft'), ('draft', 'Draft'),
('review', 'Review'), ('review', 'Review'),
('published', 'Published'), ('approved', 'Approved'), # Ready for publishing to external site
('published', 'Published'), # Actually published on external site
] ]
status = models.CharField( status = models.CharField(
max_length=50, max_length=50,

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.9 on 2026-01-01 06:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('writer', '0013_add_caption_to_images'),
]
operations = [
migrations.CreateModel(
name='ImagePrompts',
fields=[
],
options={
'verbose_name': 'Image Prompt',
'verbose_name_plural': 'Image Prompts',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('writer.images',),
),
migrations.AlterField(
model_name='content',
name='status',
field=models.CharField(choices=[('draft', 'Draft'), ('review', 'Review'), ('approved', 'Approved'), ('published', 'Published')], db_index=True, default='draft', help_text='Content status', max_length=50),
),
]

View File

@@ -11,8 +11,8 @@ class TasksSerializer(serializers.ModelSerializer):
"""Serializer for Tasks model - Stage 1 refactored""" """Serializer for Tasks model - Stage 1 refactored"""
cluster_name = serializers.SerializerMethodField() cluster_name = serializers.SerializerMethodField()
sector_name = serializers.SerializerMethodField() sector_name = serializers.SerializerMethodField()
site_id = serializers.IntegerField(write_only=True, required=False) site_id = serializers.IntegerField(read_only=True)
sector_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Tasks model = Tasks
@@ -162,8 +162,8 @@ class ContentSerializer(serializers.ModelSerializer):
has_image_prompts = serializers.SerializerMethodField() has_image_prompts = serializers.SerializerMethodField()
image_status = serializers.SerializerMethodField() image_status = serializers.SerializerMethodField()
has_generated_images = serializers.SerializerMethodField() has_generated_images = serializers.SerializerMethodField()
site_id = serializers.IntegerField(write_only=True, required=False) site_id = serializers.IntegerField(read_only=True)
sector_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Content model = Content
@@ -300,8 +300,8 @@ class ContentSerializer(serializers.ModelSerializer):
class ContentTaxonomySerializer(serializers.ModelSerializer): class ContentTaxonomySerializer(serializers.ModelSerializer):
"""Serializer for ContentTaxonomy model - Stage 1 refactored""" """Serializer for ContentTaxonomy model - Stage 1 refactored"""
content_count = serializers.SerializerMethodField() content_count = serializers.SerializerMethodField()
site_id = serializers.IntegerField(write_only=True, required=False) site_id = serializers.IntegerField(read_only=True)
sector_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = ContentTaxonomy model = ContentTaxonomy

View File

@@ -751,6 +751,19 @@ class ContentViewSet(SiteSectorModelViewSet):
'source', 'source',
] ]
def get_queryset(self):
"""Override to support status__in filtering for multiple statuses"""
queryset = super().get_queryset()
# Support status__in query param (comma-separated list of statuses)
status_in = self.request.query_params.get('status__in', None)
if status_in:
statuses = [s.strip() for s in status_in.split(',') if s.strip()]
if statuses:
queryset = queryset.filter(status__in=statuses)
return queryset
def perform_create(self, serializer): def perform_create(self, serializer):
"""Override to check monthly word limit and set account""" """Override to check monthly word limit and set account"""
user = getattr(self.request, 'user', None) user = getattr(self.request, 'user', None)

View File

@@ -15,6 +15,7 @@ import {
interface CreditAvailabilityWidgetProps { interface CreditAvailabilityWidgetProps {
availableCredits: number; availableCredits: number;
totalCredits: number; totalCredits: number;
usedCredits?: number; // Actual credits used this month from API
loading?: boolean; loading?: boolean;
} }
@@ -29,10 +30,17 @@ const OPERATION_COSTS = {
export default function CreditAvailabilityWidget({ export default function CreditAvailabilityWidget({
availableCredits, availableCredits,
totalCredits, totalCredits,
usedCredits: usedCreditsFromApi,
loading = false loading = false
}: CreditAvailabilityWidgetProps) { }: CreditAvailabilityWidgetProps) {
const usedCredits = totalCredits - availableCredits; // Use actual used credits from API if provided, otherwise calculate
const usagePercent = totalCredits > 0 ? Math.round((usedCredits / totalCredits) * 100) : 0; // Note: availableCredits may include purchased credits beyond plan allocation
const usedCredits = usedCreditsFromApi ?? 0;
// Calculate usage percentage based on plan allocation
// If available > plan, user has extra credits (purchased or carried over)
const usagePercent = totalCredits > 0 ? Math.min(Math.round((usedCredits / totalCredits) * 100), 100) : 0;
const remainingPercent = Math.max(100 - usagePercent, 0);
// Calculate available operations // Calculate available operations
const availableOps = Object.entries(OPERATION_COSTS).map(([key, config]) => ({ const availableOps = Object.entries(OPERATION_COSTS).map(([key, config]) => ({
@@ -72,11 +80,11 @@ export default function CreditAvailabilityWidget({
className={`h-2 rounded-full transition-all ${ className={`h-2 rounded-full transition-all ${
usagePercent > 90 ? 'bg-error-500' : usagePercent > 75 ? 'bg-warning-500' : 'bg-success-500' usagePercent > 90 ? 'bg-error-500' : usagePercent > 75 ? 'bg-warning-500' : 'bg-success-500'
}`} }`}
style={{ width: `${Math.max(100 - usagePercent, 0)}%` }} style={{ width: `${remainingPercent}%` }}
></div> ></div>
</div> </div>
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-600 dark:text-gray-400">
{totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used (${usagePercent}%)` : 'No credits allocated'} {totalCredits > 0 ? `${usedCredits.toLocaleString()} of ${totalCredits.toLocaleString()} used this month (${usagePercent}%)` : 'No credits allocated'}
</p> </p>
</div> </div>

View File

@@ -87,8 +87,8 @@ export default function StandardizedModuleWidget({
]; ];
// Define Writer pipeline - using correct content structure // Define Writer pipeline - using correct content structure
// Content has status: draft, review, published // Content has status: draft, review, approved, published
// totalContent = drafts + review + published // totalContent = drafts + review + approved + published
// Get writer colors from config // Get writer colors from config
const writerColors = useMemo(() => getPipelineColors('writer'), []); const writerColors = useMemo(() => getPipelineColors('writer'), []);

View File

@@ -139,7 +139,7 @@ export default function WorkflowCompletionWidget({
]; ];
// Define writer items - using "Content Pages" not "Articles" // Define writer items - using "Content Pages" not "Articles"
// Total content = drafts + review + published // Total content = drafts + review + approved + published
const totalContent = writer.contentDrafts + writer.contentReview + writer.contentPublished; const totalContent = writer.contentDrafts + writer.contentReview + writer.contentPublished;
const writerItems = [ const writerItems = [
{ label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` }, { label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` },

View File

@@ -90,6 +90,27 @@ export function createApprovedPageConfig(params: {
</div> </div>
), ),
}, },
{
key: 'status',
label: 'Status',
sortable: true,
sortField: 'status',
width: '130px',
render: (value: string, row: Content) => {
// Map internal status to user-friendly labels
const statusConfig: Record<string, { color: 'success' | 'blue' | 'amber' | 'gray'; label: string }> = {
'approved': { color: 'blue', label: 'Ready to Publish' },
'published': { color: 'success', label: row.external_id ? 'On Site' : 'Approved' },
};
const config = statusConfig[value] || { color: 'gray' as const, label: value || '-' };
return (
<Badge color={config.color} size="xs" variant="soft">
<span className="text-[11px] font-normal">{config.label}</span>
</Badge>
);
},
},
{ {
key: 'wordpress_status', key: 'wordpress_status',
label: 'Site Content Status', label: 'Site Content Status',

View File

@@ -6,7 +6,7 @@
* *
* IMPORTANT: Content table structure * IMPORTANT: Content table structure
* - Tasks is separate table (has status: queued, processing, completed, failed) * - Tasks is separate table (has status: queued, processing, completed, failed)
* - Content table has status field: 'draft', 'review', 'published' (approved) * - Content table has status field: 'draft', 'review', 'approved', 'published'
* - Images is separate table linked to content * - Images is separate table linked to content
* *
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint * Credits data comes from /v1/billing/credits/usage/summary/ endpoint
@@ -43,7 +43,7 @@ export interface WriterModuleStats {
tasksCompleted: number; // Tasks with status='completed' tasksCompleted: number; // Tasks with status='completed'
contentDrafts: number; // Content with status='draft' contentDrafts: number; // Content with status='draft'
contentReview: number; // Content with status='review' contentReview: number; // Content with status='review'
contentPublished: number; // Content with status='published' (approved) contentPublished: number; // Content with status='approved' or 'published' (ready for publishing or on site)
totalContent: number; // All content regardless of status totalContent: number; // All content regardless of status
totalImages: number; totalImages: number;
} }
@@ -158,8 +158,8 @@ export function useModuleStats() {
fetchContent({ ...baseFilters, status: 'draft' }), fetchContent({ ...baseFilters, status: 'draft' }),
// Content with status='review' // Content with status='review'
fetchContent({ ...baseFilters, status: 'review' }), fetchContent({ ...baseFilters, status: 'review' }),
// Content with status='published' (approved) // Content with status='approved' or 'published' (ready for publishing or on site)
fetchContent({ ...baseFilters, status: 'published' }), fetchContent({ ...baseFilters, status__in: 'approved,published' }),
// Total content (all statuses) // Total content (all statuses)
fetchContent({ ...baseFilters }), fetchContent({ ...baseFilters }),
// Total images // Total images

View File

@@ -9,7 +9,7 @@
* *
* IMPORTANT: Content table structure * IMPORTANT: Content table structure
* - Tasks is separate table * - Tasks is separate table
* - Content table has status field: 'draft', 'review', 'published' (approved) * - Content table has status field: 'draft', 'review', 'approved', 'published'
* - Images is separate table linked to content * - Images is separate table linked to content
* *
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint * Credits data comes from /v1/billing/credits/usage/summary/ endpoint
@@ -57,7 +57,7 @@ export interface WorkflowStats {
tasksTotal: number; tasksTotal: number;
contentDrafts: number; // Content with status='draft' contentDrafts: number; // Content with status='draft'
contentReview: number; // Content with status='review' contentReview: number; // Content with status='review'
contentPublished: number; // Content with status='published' (approved) contentPublished: number; // Content with status='approved' or 'published' (ready for publishing or on site)
imagesCreated: number; imagesCreated: number;
}; };
// Credit consumption stats - detailed breakdown by operation // Credit consumption stats - detailed breakdown by operation
@@ -208,10 +208,10 @@ export function useWorkflowStats(timeFilter: TimeFilter = 'all') {
dateFilter dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=review${baseParams}${dateParam}`) ? fetchAPI(`/v1/writer/content/?page_size=1&status=review${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'review' }), : fetchContent({ ...baseFilters, status: 'review' }),
// Content with status='published' (approved) // Content with status='approved' or 'published' (ready for publishing or on site)
dateFilter dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=published${baseParams}${dateParam}`) ? fetchAPI(`/v1/writer/content/?page_size=1&status__in=approved,published${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'published' }), : fetchContent({ ...baseFilters, status__in: 'approved,published' }),
// Total images // Total images
dateFilter dateFilter
? fetchAPI(`/v1/writer/images/?page_size=1${baseParams}${dateParam}`) ? fetchAPI(`/v1/writer/images/?page_size=1${baseParams}${dateParam}`)

View File

@@ -139,7 +139,7 @@ const AutomationPage: React.FC = () => {
fetchContent({ page_size: 1, site_id: siteId }), fetchContent({ page_size: 1, site_id: siteId }),
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }), fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }), fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }), fetchContent({ page_size: 1, site_id: siteId, status__in: 'approved,published' }),
fetchImages({ page_size: 1 }), fetchImages({ page_size: 1 }),
fetchImages({ page_size: 1, status: 'pending' }), fetchImages({ page_size: 1, status: 'pending' }),
]); ]);
@@ -258,7 +258,7 @@ const AutomationPage: React.FC = () => {
fetchContent({ page_size: 1, site_id: siteId }), fetchContent({ page_size: 1, site_id: siteId }),
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }), fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }), fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }), fetchContent({ page_size: 1, site_id: siteId, status__in: 'approved,published' }),
fetchImages({ page_size: 1 }), fetchImages({ page_size: 1 }),
fetchImages({ page_size: 1, status: 'pending' }), fetchImages({ page_size: 1, status: 'pending' }),
]); ]);

View File

@@ -7,6 +7,8 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta'; import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader'; import PageHeader from '../../components/common/PageHeader';
import ComponentCard from '../../components/common/ComponentCard';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button'; import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer'; import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI, fetchSiteSectors } from '../../services/api'; import { fetchAPI, fetchSiteSectors } from '../../services/api';
@@ -23,6 +25,7 @@ import {
BoltIcon, BoltIcon,
PageIcon, PageIcon,
ArrowRightIcon, ArrowRightIcon,
ArrowUpIcon,
} from '../../icons'; } from '../../icons';
interface Site { interface Site {
@@ -251,6 +254,7 @@ export default function SiteDashboard() {
<CreditAvailabilityWidget <CreditAvailabilityWidget
availableCredits={balance?.credits_remaining ?? 0} availableCredits={balance?.credits_remaining ?? 0}
totalCredits={balance?.plan_credits_per_month ?? 0} totalCredits={balance?.plan_credits_per_month ?? 0}
usedCredits={balance?.credits_used_this_month ?? 0}
loading={loading} loading={loading}
/> />
</div> </div>

View File

@@ -59,9 +59,9 @@ export default function Approved() {
// Load total metrics for footer widget and header metrics (not affected by pagination) // Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Fetch all approved content to calculate totals // Fetch all approved+published content to calculate totals
const data = await fetchContent({ const data = await fetchContent({
status: 'published', // Backend uses 'published' for approved content status__in: 'approved,published', // Both approved and published content
page_size: 1000, // Fetch enough to count page_size: 1000, // Fetch enough to count
}); });
@@ -86,7 +86,7 @@ export default function Approved() {
loadTotalMetrics(); loadTotalMetrics();
}, [loadTotalMetrics]); }, [loadTotalMetrics]);
// Load content - filtered for approved status (API still uses 'published' internally) // Load content - filtered for approved+published status
const loadContent = useCallback(async () => { const loadContent = useCallback(async () => {
setLoading(true); setLoading(true);
setShowContent(false); setShowContent(false);
@@ -95,7 +95,7 @@ export default function Approved() {
const filters: ContentFilters = { const filters: ContentFilters = {
...(searchTerm && { search: searchTerm }), ...(searchTerm && { search: searchTerm }),
status: 'published', // Backend uses 'published' for approved content status__in: 'approved,published', // Both approved and published content
page: currentPage, page: currentPage,
page_size: pageSize, page_size: pageSize,
ordering, ordering,
@@ -221,8 +221,13 @@ export default function Approved() {
toast.warning('WordPress URL not available'); toast.warning('WordPress URL not available');
} }
} else if (action === 'edit') { } else if (action === 'edit') {
// Navigate to content editor (if exists) or show edit modal // Navigate to content editor
navigate(`/writer/content?id=${row.id}`); if (row.site_id) {
navigate(`/sites/${row.site_id}/posts/${row.id}/edit`);
} else {
// Fallback if site_id not available
toast.warning('Unable to edit: Site information not available');
}
} }
}, [toast, loadContent, navigate]); }, [toast, loadContent, navigate]);

View File

@@ -81,11 +81,11 @@ export default function Content() {
}); });
setTotalReview(reviewRes.count || 0); setTotalReview(reviewRes.count || 0);
// Get content with status='published' // Get content with status='approved' or 'published' (ready for publishing or on site)
const publishedRes = await fetchContent({ const publishedRes = await fetchContent({
page_size: 1, page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }), ...(activeSector?.id && { sector_id: activeSector.id }),
status: 'published', status__in: 'approved,published',
}); });
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);

View File

@@ -1385,7 +1385,7 @@ export interface ContentImage {
export interface ContentImagesGroup { export interface ContentImagesGroup {
content_id: number; content_id: number;
content_title: string; content_title: string;
content_status: 'draft' | 'review' | 'publish'; content_status: 'draft' | 'review' | 'approved' | 'published';
featured_image: ContentImage | null; featured_image: ContentImage | null;
in_article_images: ContentImage[]; in_article_images: ContentImage[];
overall_status: 'pending' | 'partial' | 'complete' | 'failed'; overall_status: 'pending' | 'partial' | 'complete' | 'failed';
@@ -2197,6 +2197,7 @@ export async function deleteAuthorProfile(id: number): Promise<void> {
export interface ContentFilters { export interface ContentFilters {
search?: string; search?: string;
status?: string; status?: string;
status__in?: string; // Comma-separated list of statuses (e.g., 'approved,published')
content_type?: string; content_type?: string;
content_structure?: string; content_structure?: string;
source?: string; source?: string;
@@ -2215,9 +2216,11 @@ export interface Content {
content_html: string; content_html: string;
content_type: string; content_type: string;
content_structure: string; content_structure: string;
status: 'draft' | 'published'; status: 'draft' | 'review' | 'approved' | 'published';
source: 'igny8' | 'wordpress'; source: 'igny8' | 'wordpress';
// Relations // Relations
site_id?: number;
sector_id?: number;
cluster_id: number; cluster_id: number;
cluster_name?: string | null; cluster_name?: string | null;
sector_name?: string | null; sector_name?: string | null;
@@ -2282,6 +2285,7 @@ export async function fetchContent(filters: ContentFilters = {}): Promise<Conten
if (filters.search) params.append('search', filters.search); if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status); if (filters.status) params.append('status', filters.status);
if (filters.status__in) params.append('status__in', filters.status__in);
if (filters.task_id) params.append('task_id', filters.task_id.toString()); 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.site_id) params.append('site_id', filters.site_id.toString());
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString()); if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());

View File

@@ -792,6 +792,12 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
if (statusLower === 'generated' || statusLower === 'published' || statusLower === 'complete') { if (statusLower === 'generated' || statusLower === 'published' || statusLower === 'complete') {
return 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400'; return 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400';
} }
if (statusLower === 'approved') {
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
}
if (statusLower === 'review') {
return 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400';
}
if (statusLower === 'pending' || statusLower === 'draft') { if (statusLower === 'pending' || statusLower === 'draft') {
return 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400'; return 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400';
} }
@@ -801,6 +807,18 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'; return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
}; };
const getStatusLabel = (status: string, hasExternalId?: boolean) => {
const statusLower = status.toLowerCase();
// Map status to user-friendly labels
const statusLabels: Record<string, string> = {
'draft': 'Draft',
'review': 'In Review',
'approved': 'Ready to Publish',
'published': hasExternalId ? 'On Site' : 'Approved',
};
return statusLabels[statusLower] || status.charAt(0).toUpperCase() + status.slice(1);
};
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
@@ -824,7 +842,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<FileTextIcon className="w-6 h-6" /> <FileTextIcon className="w-6 h-6" />
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(content.status)}`}> <span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(content.status)}`}>
{content.status} {getStatusLabel(content.status, !!content.external_id)}
</span> </span>
</div> </div>
<h1 className="text-3xl font-bold mb-2"> <h1 className="text-3xl font-bold mb-2">