Section 1 & 2 - #Migration Run
This commit is contained in:
@@ -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"""
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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'), []);
|
||||||
|
|
||||||
|
|||||||
@@ -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})` },
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
|||||||
@@ -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' }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user