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

@@ -15,6 +15,7 @@ import {
interface CreditAvailabilityWidgetProps {
availableCredits: number;
totalCredits: number;
usedCredits?: number; // Actual credits used this month from API
loading?: boolean;
}
@@ -29,10 +30,17 @@ const OPERATION_COSTS = {
export default function CreditAvailabilityWidget({
availableCredits,
totalCredits,
usedCredits: usedCreditsFromApi,
loading = false
}: CreditAvailabilityWidgetProps) {
const usedCredits = totalCredits - availableCredits;
const usagePercent = totalCredits > 0 ? Math.round((usedCredits / totalCredits) * 100) : 0;
// Use actual used credits from API if provided, otherwise calculate
// 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
const availableOps = Object.entries(OPERATION_COSTS).map(([key, config]) => ({
@@ -72,11 +80,11 @@ export default function CreditAvailabilityWidget({
className={`h-2 rounded-full transition-all ${
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>
<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>
</div>

View File

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

View File

@@ -139,7 +139,7 @@ export default function WorkflowCompletionWidget({
];
// 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 writerItems = [
{ label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` },

View File

@@ -90,6 +90,27 @@ export function createApprovedPageConfig(params: {
</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',
label: 'Site Content Status',

View File

@@ -6,7 +6,7 @@
*
* IMPORTANT: Content table structure
* - 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
*
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint
@@ -43,7 +43,7 @@ export interface WriterModuleStats {
tasksCompleted: number; // Tasks with status='completed'
contentDrafts: number; // Content with status='draft'
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
totalImages: number;
}
@@ -158,8 +158,8 @@ export function useModuleStats() {
fetchContent({ ...baseFilters, status: 'draft' }),
// Content with status='review'
fetchContent({ ...baseFilters, status: 'review' }),
// Content with status='published' (approved)
fetchContent({ ...baseFilters, status: 'published' }),
// Content with status='approved' or 'published' (ready for publishing or on site)
fetchContent({ ...baseFilters, status__in: 'approved,published' }),
// Total content (all statuses)
fetchContent({ ...baseFilters }),
// Total images

View File

@@ -9,7 +9,7 @@
*
* IMPORTANT: Content table structure
* - 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
*
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint
@@ -57,7 +57,7 @@ export interface WorkflowStats {
tasksTotal: number;
contentDrafts: number; // Content with status='draft'
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;
};
// Credit consumption stats - detailed breakdown by operation
@@ -208,10 +208,10 @@ export function useWorkflowStats(timeFilter: TimeFilter = 'all') {
dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=review${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'review' }),
// Content with status='published' (approved)
// Content with status='approved' or 'published' (ready for publishing or on site)
dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=published${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'published' }),
? fetchAPI(`/v1/writer/content/?page_size=1&status__in=approved,published${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status__in: 'approved,published' }),
// Total images
dateFilter
? 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, status: 'draft' }),
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, status: 'pending' }),
]);
@@ -258,7 +258,7 @@ const AutomationPage: React.FC = () => {
fetchContent({ page_size: 1, site_id: siteId }),
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: 'published' }),
fetchContent({ page_size: 1, site_id: siteId, status__in: 'approved,published' }),
fetchImages({ page_size: 1 }),
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 PageMeta from '../../components/common/PageMeta';
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 { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI, fetchSiteSectors } from '../../services/api';
@@ -23,6 +25,7 @@ import {
BoltIcon,
PageIcon,
ArrowRightIcon,
ArrowUpIcon,
} from '../../icons';
interface Site {
@@ -251,6 +254,7 @@ export default function SiteDashboard() {
<CreditAvailabilityWidget
availableCredits={balance?.credits_remaining ?? 0}
totalCredits={balance?.plan_credits_per_month ?? 0}
usedCredits={balance?.credits_used_this_month ?? 0}
loading={loading}
/>
</div>

View File

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

View File

@@ -81,11 +81,11 @@ export default function Content() {
});
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({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'published',
status__in: 'approved,published',
});
setTotalPublished(publishedRes.count || 0);

View File

@@ -1385,7 +1385,7 @@ export interface ContentImage {
export interface ContentImagesGroup {
content_id: number;
content_title: string;
content_status: 'draft' | 'review' | 'publish';
content_status: 'draft' | 'review' | 'approved' | 'published';
featured_image: ContentImage | null;
in_article_images: ContentImage[];
overall_status: 'pending' | 'partial' | 'complete' | 'failed';
@@ -2197,6 +2197,7 @@ export async function deleteAuthorProfile(id: number): Promise<void> {
export interface ContentFilters {
search?: string;
status?: string;
status__in?: string; // Comma-separated list of statuses (e.g., 'approved,published')
content_type?: string;
content_structure?: string;
source?: string;
@@ -2215,9 +2216,11 @@ export interface Content {
content_html: string;
content_type: string;
content_structure: string;
status: 'draft' | 'published';
status: 'draft' | 'review' | 'approved' | 'published';
source: 'igny8' | 'wordpress';
// Relations
site_id?: number;
sector_id?: number;
cluster_id: number;
cluster_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.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.site_id) params.append('site_id', filters.site_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') {
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') {
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';
};
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 (
<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">
@@ -824,7 +842,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
<div className="flex items-center gap-3 mb-3">
<FileTextIcon className="w-6 h-6" />
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(content.status)}`}>
{content.status}
{getStatusLabel(content.status, !!content.external_id)}
</span>
</div>
<h1 className="text-3xl font-bold mb-2">