diff --git a/frontend/App.tsx b/frontend/App.tsx index c2948b2..d498393 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -510,6 +510,29 @@ const App: React.FC = () => { } }; + const handleDeleteCampaign = async (campaignName: string) => { + const campaign = campaigns.find(c => c.name === campaignName); + if (!campaign?._id) { + setError('Campaign not found.'); + return; + } + + try { + await apiService.deleteCampaign(campaign._id); + + // Update local state after successful deletion + setCampaigns(prev => prev.filter(c => c.name !== campaignName)); + setCampaignProofs(prev => { + const newProofs = { ...prev }; + delete newProofs[campaignName]; + return newProofs; + }); + } catch (error) { + console.error('Error deleting campaign:', error); + setError('Failed to delete campaign. Please try again.'); + } + }; + // --- SETTINGS HANDLERS (NOW USE API) --- // Helper to refresh dropdown options from API @@ -791,6 +814,7 @@ const App: React.FC = () => { onRetryAnalysis={handleRetryAnalysis} onCampaignStatusChange={handleCampaignStatusChange} onDeleteProof={handleDeleteProof} + onDeleteCampaign={handleDeleteCampaign} onFlagSubmit={handleFlagSubmit} onResolveSubmit={handleResolveSubmit} />; diff --git a/frontend/components/Campaigns.tsx b/frontend/components/Campaigns.tsx index 54c8410..617f4f9 100755 --- a/frontend/components/Campaigns.tsx +++ b/frontend/components/Campaigns.tsx @@ -268,16 +268,35 @@ const OverallStatusBadge: React.FC<{ status: OverallStatus }> = ({ status }) => }; -const CampaignList: React.FC<{ - onSelectCampaign: (name: string) => void; +const CampaignList: React.FC<{ + onSelectCampaign: (name: string) => void; campaigns: typeof initialCampaigns; onOpenModal: () => void; onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void; -}> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange }) => { + onDeleteCampaign: (campaign: typeof initialCampaigns[0]) => void; +}> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange, onDeleteCampaign }) => { const [showCompleted, setShowCompleted] = useState(true); + const [selectedCampaigns, setSelectedCampaigns] = useState>(new Set()); + const [campaignToDelete, setCampaignToDelete] = useState(null); + const [showBulkDeleteModal, setShowBulkDeleteModal] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const filteredCampaigns = showCompleted ? campaigns : campaigns.filter(p => p.status !== 'Completed'); + // Clear selection when campaigns list changes (e.g., after deletion) + useEffect(() => { + setSelectedCampaigns(prev => { + const currentNames = new Set(filteredCampaigns.map(c => c.name)); + const newSelection = new Set(); + prev.forEach(name => { + if (currentNames.has(name)) { + newSelection.add(name); + } + }); + return newSelection; + }); + }, [filteredCampaigns]); + const getStatusSelectClasses = (status: string) => { let baseClasses = 'w-full text-center px-2.5 py-0.5 text-xs font-semibold rounded-full border border-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent appearance-none cursor-pointer'; let colorClasses = ''; @@ -294,8 +313,77 @@ const CampaignList: React.FC<{ return `${baseClasses} ${colorClasses}`; }; + const handleSelectAll = () => { + if (selectedCampaigns.size === filteredCampaigns.length) { + setSelectedCampaigns(new Set()); + } else { + setSelectedCampaigns(new Set(filteredCampaigns.map(c => c.name))); + } + }; + + const handleSelectCampaign = (campaignName: string, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedCampaigns(prev => { + const newSet = new Set(prev); + if (newSet.has(campaignName)) { + newSet.delete(campaignName); + } else { + newSet.add(campaignName); + } + return newSet; + }); + }; + + const handleSingleDelete = (campaign: typeof initialCampaigns[0], e: React.MouseEvent) => { + e.stopPropagation(); + setCampaignToDelete(campaign); + }; + + const handleConfirmSingleDelete = async () => { + if (campaignToDelete) { + setIsDeleting(true); + await onDeleteCampaign(campaignToDelete); + setCampaignToDelete(null); + setIsDeleting(false); + } + }; + + const handleConfirmBulkDelete = async () => { + setIsDeleting(true); + const campaignsToDelete = filteredCampaigns.filter(c => selectedCampaigns.has(c.name)); + for (const campaign of campaignsToDelete) { + await onDeleteCampaign(campaign); + } + setSelectedCampaigns(new Set()); + setShowBulkDeleteModal(false); + setIsDeleting(false); + }; + + const isAllSelected = filteredCampaigns.length > 0 && selectedCampaigns.size === filteredCampaigns.length; + const isIndeterminate = selectedCampaigns.size > 0 && selectedCampaigns.size < filteredCampaigns.length; + return (
+ setCampaignToDelete(null)} + onConfirm={handleConfirmSingleDelete} + campaignName={campaignToDelete?.name || ''} + proofCount={campaignToDelete?.proofs || 0} + isBulk={false} + selectedCount={1} + /> + + setShowBulkDeleteModal(false)} + onConfirm={handleConfirmBulkDelete} + campaignName="" + proofCount={0} + isBulk={true} + selectedCount={selectedCampaigns.size} + /> +
@@ -319,23 +407,76 @@ const CampaignList: React.FC<{
+ {/* Bulk Actions Bar */} + {selectedCampaigns.size > 0 && ( +
+
+ + {selectedCampaigns.size} campaign{selectedCampaigns.size !== 1 ? 's' : ''} selected + + +
+ +
+ )} +
+ + {filteredCampaigns.map((campaign) => { + const isSelected = selectedCampaigns.has(campaign.name); return ( - onSelectCampaign(campaign.name)}> + onSelectCampaign(campaign.name)} + > + + ); })} @@ -740,6 +892,57 @@ const DeleteConfirmationModal: React.FC<{ ); }; +const CampaignDeleteConfirmationModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + campaignName: string; + proofCount: number; + isBulk: boolean; + selectedCount: number; +}> = ({ isOpen, onClose, onConfirm, campaignName, proofCount, isBulk, selectedCount }) => { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > +

Confirm Deletion

+

+ {isBulk ? ( + <>Are you sure you want to permanently delete {selectedCount} selected campaign{selectedCount !== 1 ? 's' : ''}? All associated proofs will also be deleted. This action cannot be undone. + ) : ( + <>Are you sure you want to permanently delete the campaign "{campaignName}"? {proofCount > 0 && <>This will also delete {proofCount} proof{proofCount !== 1 ? 's' : ''}.} This action cannot be undone. + )} +

+
+ + +
+
+
+ ); +}; + const CampaignDetail: React.FC<{ campaignName: string; onBack: () => void; @@ -1398,16 +1601,17 @@ interface CampaignsProps { onRetryAnalysis: (campaignName: string, tempId: string) => void; onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void; onDeleteProof: (campaignName: string, proofName: string) => void; + onDeleteCampaign: (campaignName: string) => Promise; onFlagSubmit: (flagData: Omit) => void; onResolveSubmit: (resolveData: Omit) => void; } -export const Campaigns: React.FC = ({ - selectedCampaign, - selectedProof, - onSelectCampaign, - onSelectProof, - onBackToCampaignsList, +export const Campaigns: React.FC = ({ + selectedCampaign, + selectedProof, + onSelectCampaign, + onSelectProof, + onBackToCampaignsList, onBackToCampaignDetails, campaigns, campaignProofs, @@ -1417,6 +1621,7 @@ export const Campaigns: React.FC = ({ onRetryAnalysis, onCampaignStatusChange, onDeleteProof, + onDeleteCampaign, onFlagSubmit, onResolveSubmit, }) => { @@ -1471,11 +1676,12 @@ export const Campaigns: React.FC = ({ onAddCampaign={onAddNewCampaign} brandGuidelines={dropdownOptions.brandGuidelines} /> - setIsModalOpen(true)} onCampaignStatusChange={onCampaignStatusChange} + onDeleteCampaign={(campaign) => onDeleteCampaign(campaign.name)} /> );
+ { + if (el) el.indeterminate = isIndeterminate; + }} + onChange={handleSelectAll} + className="h-4 w-4 text-brand-accent border-gray-300 rounded focus:ring-brand-accent cursor-pointer" + aria-label="Select all campaigns" + /> + Campaign Name Proofs Status Created By Owning Agency Last ModifiedActions
+ {}} // Handled by onClick + onClick={(e) => handleSelectCampaign(campaign.name, e)} + className="h-4 w-4 text-brand-accent border-gray-300 rounded focus:ring-brand-accent cursor-pointer" + aria-label={`Select ${campaign.name}`} + /> + {campaign.name} {campaign.proofs} @@ -356,6 +497,17 @@ const CampaignList: React.FC<{ {campaign.agencyLead} {campaign.agency} {formatDate(campaign.lastModified)} + +