diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx index 89036e60..36d9ac5a 100644 --- a/frontend/src/pages/Planner/Keywords.tsx +++ b/frontend/src/pages/Planner/Keywords.tsx @@ -342,7 +342,7 @@ export default function Keywords() { }; // Import/Export handlers - const { handleExport, handleImportClick, ImportModal } = useKeywordsImportExport( + const { handleExport, handleImportClick: baseHandleImportClick, ImportModal } = useKeywordsImportExport( () => { toast.success('Import successful', 'Keywords imported successfully.'); loadKeywords(); @@ -350,12 +350,34 @@ export default function Keywords() { (error) => { toast.error('Import failed', error.message); }, - // Pass active site_id and active sector_id for import - activeSite && activeSector + // Pass active site_id and active sector_id for import, but validate sector belongs to site + activeSite && activeSector && activeSector.site_id === activeSite.id ? { site_id: activeSite.id, sector_id: activeSector.id } + : activeSite // Only pass site_id if sector doesn't belong to site + ? { site_id: activeSite.id } : undefined ); + // Custom import click handler with validation + const handleImportClick = () => { + // Validate that sector belongs to the selected site + if (activeSite && activeSector && activeSector.site_id !== activeSite.id) { + toast.error('Import failed', `Selected sector "${activeSector.name}" does not belong to site "${activeSite.name}". Please select a sector from the current site.`); + return; + } + + // Debug logging + console.log('Import attempt:', { + site_id: activeSite?.id, + site_name: activeSite?.name, + sector_id: activeSector?.id, + sector_name: activeSector?.name, + sector_site_id: activeSector?.site_id + }); + + baseHandleImportClick(); + }; + // Handle bulk actions (delete, export, update_status are now handled by TablePageTemplate) // This is only for actions that don't have modals (like auto_cluster) const handleBulkAction = useCallback(async (action: string, ids: string[]) => { diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx index da91d2cb..7d853417 100644 --- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx +++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx @@ -404,29 +404,84 @@ export default function IndustriesSectorsKeywords() { const formData = new FormData(); formData.append('file', importFile); - const response = await fetch('/api/v1/auth/seed-keywords/import_seed_keywords/', { - method: 'POST', - headers: { - 'Authorization': `Token ${localStorage.getItem('authToken')}`, - }, - body: formData, - }); + // Get token from auth store (consistent with other API calls) + const getAuthToken = () => { + try { + const authStorage = localStorage.getItem('auth-storage'); + if (authStorage) { + const parsed = JSON.parse(authStorage); + return parsed?.state?.token || null; + } + } catch (e) { + // Ignore parsing errors + } + return null; + }; - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Import failed'); + const token = getAuthToken(); + const headers: HeadersInit = {}; + if (token) { + headers['Authorization'] = `Bearer ${token}`; } - const result = await response.json(); - toast.success(`Successfully imported ${result.created || 0} keywords`); + const response = await fetch('/api/v1/auth/seed-keywords/import_seed_keywords/', { + method: 'POST', + headers, + body: formData, + credentials: 'include', // Add credentials for consistency + }); + + let errorMessage = 'Import failed'; - // Reset and close modal - setImportFile(null); - setIsImportModalOpen(false); + // Handle common HTTP status codes + if (response.status === 404) { + errorMessage = 'Import endpoint not found. Please check if the backend service is running.'; + } else if (response.status === 429) { + errorMessage = 'Too many requests. Please wait a moment before trying again.'; + } else if (response.status === 403) { + errorMessage = 'Access denied. Admin privileges required for seed keyword import.'; + } - // Reload keywords - if (activeSite) { - loadSeedKeywords(); + try { + const result = await response.json(); + + if (!response.ok) { + // Try to get error message from response (override status-based message if available) + if (result.error) { + errorMessage = result.error; + } else if (result.message) { + errorMessage = result.message; + } + throw new Error(errorMessage); + } + + // Success case + const importedCount = result.data?.imported || result.imported || 0; + const skippedCount = result.data?.skipped || result.skipped || 0; + const errors = result.data?.errors || result.errors || []; + + toast.success( + `Successfully imported ${importedCount} keyword(s)${skippedCount > 0 ? `, ${skippedCount} skipped` : ''}` + ); + + if (errors.length > 0) { + toast.info(`Errors: ${errors.slice(0, 3).join(', ')}`); + } + + // Reset and close modal + setImportFile(null); + setIsImportModalOpen(false); + + // Reload keywords + if (activeSite) { + loadSeedKeywords(); + } + } catch (jsonError: any) { + // If JSON parsing fails, handle it gracefully + if (!response.ok) { + throw new Error(errorMessage || `Server error: ${response.status}`); + } + throw jsonError; } } catch (error: any) { console.error('Import error:', error); @@ -631,6 +686,21 @@ export default function IndustriesSectorsKeywords() { title="Add Keywords" badge={{ icon: , color: 'blue' }} /> + + {/* Action Buttons */} + {isAdmin && ( +
+ +
+ )} + - - Import Keywords - - ) : undefined - } + customActions={undefined} pagination={{ currentPage, totalPages, @@ -702,6 +761,7 @@ export default function IndustriesSectorsKeywords() { setImportFile(null); } }} + className="max-w-md" >

diff --git a/frontend/src/utils/table-import-export.ts b/frontend/src/utils/table-import-export.ts index 1dfb5840..3d3a219f 100644 --- a/frontend/src/utils/table-import-export.ts +++ b/frontend/src/utils/table-import-export.ts @@ -155,8 +155,29 @@ export const importTableData = async ( const queryString = params.toString(); const url = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`; + // Get authentication token (consistent with other API calls) + const getAuthToken = () => { + try { + const authStorage = localStorage.getItem('auth-storage'); + if (authStorage) { + const parsed = JSON.parse(authStorage); + return parsed?.state?.token || null; + } + } catch (e) { + // Ignore parsing errors + } + return null; + }; + + const token = getAuthToken(); + const headers: HeadersInit = {}; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const response = await fetch(url, { method: 'POST', + headers, body: formData, credentials: 'include', }); diff --git a/keywordsplanner.csv b/keywordsplanner.csv new file mode 100644 index 00000000..6879b98c --- /dev/null +++ b/keywordsplanner.csv @@ -0,0 +1,4 @@ +keyword,volume,difficulty,intent,status +keyword 1,700,52,informational,pending +keyword 2,950,92,commercial,pending +keyword 3,850,76,commercial,pending diff --git a/seedKeywords_fixed.csv b/seedKeywords_fixed.csv new file mode 100644 index 00000000..4b13335d --- /dev/null +++ b/seedKeywords_fixed.csv @@ -0,0 +1,4 @@ +keyword,volume,difficulty,intent,industry_name,sector_name +keyword 1,550,28,commercial,Home & Garden,Home Decor +keyword 2,700,52,informational,Home & Garden,Home Decor +keyword 3,950,92,commercial,Home & Garden,Home Decor