From d97a96a7c42d7349cc38bda5cb336fab870ab49d Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 1 Dec 2025 04:51:09 +0000 Subject: [PATCH] asd --- backend/igny8_core/modules/planner/views.py | 21 +++- .../pages/Planner/KeywordOpportunities.tsx | 89 ++++++++++++--- .../pages/Setup/IndustriesSectorsKeywords.tsx | 107 +++++++++++++----- frontend/src/services/api.ts | 4 +- 4 files changed, 173 insertions(+), 48 deletions(-) diff --git a/backend/igny8_core/modules/planner/views.py b/backend/igny8_core/modules/planner/views.py index b4eee2e6..6a1b5394 100644 --- a/backend/igny8_core/modules/planner/views.py +++ b/backend/igny8_core/modules/planner/views.py @@ -321,12 +321,29 @@ class KeywordViewSet(SiteSectorModelViewSet): try: # Validate industry/sector match if site.industry != seed_keyword.industry: - errors.append(f"SeedKeyword '{seed_keyword.keyword}' industry mismatch") + errors.append( + f"Keyword '{seed_keyword.keyword}': industry mismatch " + f"(site={site.industry.name if site.industry else 'None'}, " + f"seed={seed_keyword.industry.name if seed_keyword.industry else 'None'})" + ) + skipped_count += 1 + continue + + # Check if sector has industry_sector set + if not sector.industry_sector: + errors.append( + f"Keyword '{seed_keyword.keyword}': sector '{sector.name}' has no industry_sector set. " + f"Please update the sector to reference an industry sector." + ) skipped_count += 1 continue if sector.industry_sector != seed_keyword.sector: - errors.append(f"SeedKeyword '{seed_keyword.keyword}' sector mismatch") + errors.append( + f"Keyword '{seed_keyword.keyword}': sector mismatch " + f"(sector={sector.industry_sector.name if sector.industry_sector else 'None'}, " + f"seed={seed_keyword.sector.name if seed_keyword.sector else 'None'})" + ) skipped_count += 1 continue diff --git a/frontend/src/pages/Planner/KeywordOpportunities.tsx b/frontend/src/pages/Planner/KeywordOpportunities.tsx index 34b3ce03..e9593d94 100644 --- a/frontend/src/pages/Planner/KeywordOpportunities.tsx +++ b/frontend/src/pages/Planner/KeywordOpportunities.tsx @@ -297,25 +297,43 @@ export default function KeywordOpportunities() { ); if (result.success) { - toast.success(`Successfully added ${result.created} keyword(s) to workflow`); + // Show success message with created count + if (result.created > 0) { + toast.success(`Successfully added ${result.created} keyword(s) to workflow`); + } - // Track these as recently added to preserve state during reload - seedKeywordIds.forEach(id => { - recentlyAddedRef.current.add(id); - }); + // Show skipped count if any + if (result.skipped > 0) { + toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`); + } + + // Show detailed errors if any + if (result.errors && result.errors.length > 0) { + result.errors.forEach((error: string) => { + toast.error(error, { duration: 8000 }); + }); + } + + // Only track and mark as added if actually created + if (result.created > 0) { + // Track these as recently added to preserve state during reload + seedKeywordIds.forEach(id => { + recentlyAddedRef.current.add(id); + }); + + // Immediately update state to mark keywords as added - this gives instant feedback + setSeedKeywords(prevKeywords => + prevKeywords.map(kw => + seedKeywordIds.includes(kw.id) + ? { ...kw, isAdded: true } + : kw + ) + ); + } // Clear selection setSelectedIds([]); - // Immediately update state to mark keywords as added - this gives instant feedback - setSeedKeywords(prevKeywords => - prevKeywords.map(kw => - seedKeywordIds.includes(kw.id) - ? { ...kw, isAdded: true } - : kw - ) - ); - // Don't reload immediately - the state is already updated // The recentlyAddedRef will ensure they stay marked as added // Only reload if user changes filters/pagination @@ -546,6 +564,13 @@ export default function KeywordOpportunities() { ], }, ], + bulkActions: !activeSector ? [] : [ + { + key: 'add_selected_to_workflow', + label: 'Add Selected to Workflow', + variant: 'primary' as const, + }, + ], }; }, [activeSector]); @@ -555,6 +580,30 @@ export default function KeywordOpportunities() { title="Keyword Opportunities" badge={{ icon: , color: 'orange' }} /> + + {/* Show info banner when no sector is selected */} + {!activeSector && activeSite && ( +
+
+
+
+ + + +
+
+

+ Select a Sector to Add Keywords +

+

+ Please select a sector from the dropdown above to enable adding keywords to your workflow. Keywords must be added to a specific sector. +

+
+
+
+
+ )} + { if (actionKey === 'add_to_workflow') { + // Check if sector is selected + if (!activeSector) { + toast.error('Please select a sector first'); + return; + } // Don't allow adding already-added keywords if (row.isAdded) { toast.info('This keyword is already added to workflow'); @@ -589,12 +643,17 @@ export default function KeywordOpportunities() { await handleAddToWorkflow([row.id]); } }} + bulkActions={pageConfig.bulkActions} onBulkAction={async (actionKey: string, ids: string[]) => { if (actionKey === 'add_selected_to_workflow') { + if (!activeSector) { + toast.error('Please select a sector first'); + return; + } await handleBulkAddSelected(ids); } }} - onCreate={handleAddAll} + onCreate={activeSector ? handleAddAll : undefined} createLabel="Add All to Workflow" onCreateIcon={} pagination={{ diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx index da91d2cb..fdc37751 100644 --- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx +++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx @@ -317,24 +317,42 @@ export default function IndustriesSectorsKeywords() { ); if (result.success) { - toast.success(`Successfully added ${result.created} keyword(s) to workflow`); + // Show success message with created count + if (result.created > 0) { + toast.success(`Successfully added ${result.created} keyword(s) to workflow`); + } - // Track as recently added - seedKeywordIds.forEach(id => { - recentlyAddedRef.current.add(id); - }); + // Show skipped count if any + if (result.skipped > 0) { + toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`); + } + + // Show detailed errors if any + if (result.errors && result.errors.length > 0) { + result.errors.forEach((error: string) => { + toast.error(error, { duration: 8000 }); + }); + } + + // Only track and mark as added if actually created + if (result.created > 0) { + // Track as recently added + seedKeywordIds.forEach(id => { + recentlyAddedRef.current.add(id); + }); + + // Update state - mark as added + setSeedKeywords(prevKeywords => + prevKeywords.map(kw => + seedKeywordIds.includes(kw.id) + ? { ...kw, isAdded: true } + : kw + ) + ); + } // Clear selection setSelectedIds([]); - - // Update state immediately - setSeedKeywords(prevKeywords => - prevKeywords.map(kw => - seedKeywordIds.includes(kw.id) - ? { ...kw, isAdded: true } - : kw - ) - ); } else { toast.error(`Failed to add keywords: ${result.errors?.join(', ') || 'Unknown error'}`); } @@ -531,20 +549,28 @@ export default function IndustriesSectorsKeywords() { label: '', sortable: false, align: 'right' as const, - render: (_value: any, row: SeedKeyword & { isAdded?: boolean }) => ( - - ), + render: (_value: any, row: SeedKeyword & { isAdded?: boolean }) => { + const isDisabled = !activeSector || row.isAdded; + const buttonText = row.isAdded ? 'Added' : 'Add to Workflow'; + + return ( +
+ +
+ ); + }, }, ], filters: [ @@ -580,7 +606,7 @@ export default function IndustriesSectorsKeywords() { ], }, ], - bulkActions: [ + bulkActions: !activeSector ? [] : [ { key: 'add_selected_to_workflow', label: 'Add Selected to Workflow', @@ -631,6 +657,29 @@ export default function IndustriesSectorsKeywords() { title="Add Keywords" badge={{ icon: , color: 'blue' }} /> + {/* Show info banner when no sector is selected */} + {!activeSector && activeSite && ( +
+
+
+
+ + + +
+
+

+ Select a Sector to Add Keywords +

+

+ Please select a sector from the dropdown above to enable adding keywords to your workflow. Keywords must be added to a specific sector. +

+
+
+
+
+ )} + { +export async function addSeedKeywordsToWorkflow(seedKeywordIds: number[], siteId: number, sectorId: number): Promise<{ success: boolean; created: number; skipped?: number; errors?: string[] }> { try { // fetchAPI extracts data from unified format {success: true, data: {...}} - // So response is already the data object: {created: X, ...} + // So response is already the data object: {created: X, skipped: X, errors: [...]} const response = await fetchAPI('/v1/planner/keywords/bulk_add_from_seed/', { method: 'POST', body: JSON.stringify({