seed keywords

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-29 23:30:22 +00:00
parent 0100db62c0
commit d2f3f3ef97
7 changed files with 830 additions and 539 deletions

View File

@@ -838,14 +838,133 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
"""Filter by industry and sector if provided."""
queryset = super().get_queryset()
industry_id = self.request.query_params.get('industry_id')
industry_name = self.request.query_params.get('industry_name')
sector_id = self.request.query_params.get('sector_id')
sector_name = self.request.query_params.get('sector_name')
if industry_id:
queryset = queryset.filter(industry_id=industry_id)
if industry_name:
queryset = queryset.filter(industry__name__icontains=industry_name)
if sector_id:
queryset = queryset.filter(sector_id=sector_id)
if sector_name:
queryset = queryset.filter(sector__name__icontains=sector_name)
return queryset
@action(detail=False, methods=['post'], url_path='import_seed_keywords', url_name='import_seed_keywords')
def import_seed_keywords(self, request):
"""
Import seed keywords from CSV (Admin/Superuser only).
Expected columns: keyword, industry_name, sector_name, volume, difficulty, intent
"""
import csv
from django.db import transaction
# Check admin/superuser permission
if not (request.user.is_staff or request.user.is_superuser):
return error_response(
error='Admin or superuser access required',
status_code=status.HTTP_403_FORBIDDEN,
request=request
)
if 'file' not in request.FILES:
return error_response(
error='No file provided',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
file = request.FILES['file']
if not file.name.endswith('.csv'):
return error_response(
error='File must be a CSV',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
# Parse CSV
decoded_file = file.read().decode('utf-8')
csv_reader = csv.DictReader(decoded_file.splitlines())
imported_count = 0
skipped_count = 0
errors = []
with transaction.atomic():
for row_num, row in enumerate(csv_reader, start=2): # Start at 2 (header is row 1)
try:
keyword_text = row.get('keyword', '').strip()
industry_name = row.get('industry_name', '').strip()
sector_name = row.get('sector_name', '').strip()
if not all([keyword_text, industry_name, sector_name]):
skipped_count += 1
continue
# Get or create industry
industry = Industry.objects.filter(name=industry_name).first()
if not industry:
errors.append(f"Row {row_num}: Industry '{industry_name}' not found")
skipped_count += 1
continue
# Get or create industry sector
sector = IndustrySector.objects.filter(
industry=industry,
name=sector_name
).first()
if not sector:
errors.append(f"Row {row_num}: Sector '{sector_name}' not found for industry '{industry_name}'")
skipped_count += 1
continue
# Check if keyword already exists
existing = SeedKeyword.objects.filter(
keyword=keyword_text,
industry=industry,
sector=sector
).first()
if existing:
skipped_count += 1
continue
# Create seed keyword
SeedKeyword.objects.create(
keyword=keyword_text,
industry=industry,
sector=sector,
volume=int(row.get('volume', 0) or 0),
difficulty=int(row.get('difficulty', 0) or 0),
intent=row.get('intent', 'informational') or 'informational',
is_active=True
)
imported_count += 1
except Exception as e:
errors.append(f"Row {row_num}: {str(e)}")
skipped_count += 1
return success_response(
data={
'imported': imported_count,
'skipped': skipped_count,
'errors': errors[:10] if errors else [] # Limit errors to first 10
},
message=f'Import completed: {imported_count} keywords imported, {skipped_count} skipped',
request=request
)
except Exception as e:
return error_response(
error=f'Failed to import keywords: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
# ============================================================================

View File

@@ -356,11 +356,13 @@ export default function App() {
} />
{/* Setup Pages */}
<Route path="/setup/industries-sectors-keywords" element={
<Route path="/setup/add-keywords" element={
<Suspense fallback={null}>
<IndustriesSectorsKeywords />
</Suspense>
} />
{/* Legacy redirect */}
<Route path="/setup/industries-sectors-keywords" element={<Navigate to="/setup/add-keywords" replace />} />
{/* Automation Module - Redirect dashboard to rules */}
<Route path="/automation" element={<Navigate to="/automation/rules" replace />} />

View File

@@ -159,6 +159,11 @@ export function useImportExport(
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Upload a CSV file (max {maxFileSize / 1024 / 1024}MB)
</p>
{filename === 'keywords' && (
<p className="text-xs text-gray-600 dark:text-gray-300 mt-2 p-2 bg-blue-50 dark:bg-blue-900/20 rounded border border-blue-200 dark:border-blue-800">
<strong>Expected columns:</strong> keyword, volume, difficulty, intent, status
</p>
)}
</div>
<div className="flex justify-end gap-4 pt-4">

View File

@@ -85,8 +85,8 @@ const AppSidebar: React.FC = () => {
const setupItems: NavItem[] = [
{
icon: <DocsIcon />,
name: "Industry, Sectors & Keywords",
path: "/setup/industries-sectors-keywords", // Merged page
name: "Add Keywords",
path: "/setup/add-keywords",
},
{
icon: <GridIcon />,

View File

@@ -260,11 +260,32 @@ export default function MasterStatus() {
setLoading(false);
}, [fetchSystemMetrics, fetchApiHealth, checkWorkflowHealth, checkIntegrationHealth]);
// Initial load and auto-refresh
// Initial load and auto-refresh (pause when page not visible)
useEffect(() => {
let interval: NodeJS.Timeout;
const handleVisibilityChange = () => {
if (document.hidden) {
// Page not visible - clear interval
if (interval) clearInterval(interval);
} else {
// Page visible - refresh and restart interval
refreshAll();
interval = setInterval(refreshAll, 30000);
}
};
// Initial setup
refreshAll();
const interval = setInterval(refreshAll, 30000); // 30 seconds
return () => clearInterval(interval);
interval = setInterval(refreshAll, 30000);
// Listen for visibility changes
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
clearInterval(interval);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [refreshAll]);
// Status badge component

File diff suppressed because it is too large Load Diff

View File

@@ -133,6 +133,15 @@ interface TablePageTemplateProps {
onRowAction?: (actionKey: string, row: any) => Promise<void>;
getItemDisplayName?: (row: any) => string; // Function to get display name from row (e.g., row.keyword or row.name)
className?: string;
// Custom actions to display in action buttons area (near column selector)
customActions?: ReactNode;
// Custom bulk actions configuration (overrides table-actions.config.ts)
bulkActions?: Array<{
key: string;
label: string;
icon?: ReactNode;
variant?: 'primary' | 'success' | 'danger';
}>;
}
export default function TablePageTemplate({
@@ -167,6 +176,8 @@ export default function TablePageTemplate({
onExport,
getItemDisplayName = (row: any) => row.name || row.keyword || row.title || String(row.id),
className = '',
customActions,
bulkActions: customBulkActions,
}: TablePageTemplateProps) {
const location = useLocation();
const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false);
@@ -181,7 +192,8 @@ export default function TablePageTemplate({
// Get actions from config (edit/delete always included)
const rowActions = tableActionsConfig?.rowActions || [];
const bulkActions = tableActionsConfig?.bulkActions || [];
// Use custom bulk actions if provided, otherwise use config
const bulkActions = customBulkActions || tableActionsConfig?.bulkActions || [];
// Selection and expanded rows state
const [selectedIds, setSelectedIds] = useState<string[]>(selection?.selectedIds || []);
@@ -737,6 +749,9 @@ export default function TablePageTemplate({
{/* Action Buttons - Right aligned */}
<div className="flex gap-2 items-center">
{/* Custom Actions */}
{customActions}
{/* Column Selector */}
<ColumnSelector
columns={columns.map(col => ({