diff --git a/components/phishing/action-area-card.tsx b/components/phishing/action-area-card.tsx index 99f38bc..4038db8 100644 --- a/components/phishing/action-area-card.tsx +++ b/components/phishing/action-area-card.tsx @@ -313,9 +313,12 @@ export function ActionAreaCard({ const [isApproving, setIsApproving] = useState(false); const [isRemediating, setIsRemediating] = useState(false); const [isMarkingFalsePositive, setIsMarkingFalsePositive] = useState(false); + const [isMarkingAccidentalReport, setIsMarkingAccidentalReport] = useState(false); const [remediateDialogOpen, setRemediateDialogOpen] = useState(false); const [falsePositiveDialogOpen, setFalsePositiveDialogOpen] = useState(false); const [falsePositiveReason, setFalsePositiveReason] = useState(''); + const [accidentalReportDialogOpen, setAccidentalReportDialogOpen] = useState(false); + const [accidentalReportReason, setAccidentalReportReason] = useState(''); const recommendedActionsKey = classification?.recommendedActions.join(',') ?? ''; @@ -409,7 +412,8 @@ export function ActionAreaCard({ // remediation. Once resolved, all three buttons stay in the DOM but are // disabled with a resolved-state tooltip. const completedAction = remediationActions.find((a) => a.status === 'completed'); - const resolved = campaignStatus === 'false_positive' || completedAction != null; + const resolved = + campaignStatus === 'false_positive' || campaignStatus === 'accidental_report' || completedAction != null; function resolvedTooltipCopy(): string { if (completedAction) { @@ -418,6 +422,9 @@ export function ActionAreaCard({ : 'an earlier date'; return `Already remediated on ${dateStr} by ${completedAction.approvedBy ?? 'unknown'}`; } + if (campaignStatus === 'accidental_report') { + return `Marked as an accidental report on ${new Date(campaignUpdatedAt).toLocaleDateString()}`; + } return `Marked as false positive on ${new Date(campaignUpdatedAt).toLocaleDateString()}`; } @@ -455,6 +462,14 @@ export function ActionAreaCard({ ? 'Cannot mark false positive — this campaign already has approved or completed remediation' : null; + const markAccidentalReportDisabledReason = !canApprove + ? 'Requires approve permission' + : resolved + ? resolvedTooltipCopy() + : hasBlockingRemediation + ? 'Cannot mark as accidental report — this campaign already has approved or completed remediation' + : null; + async function handleRemediateConfirm() { setIsRemediating(true); try { @@ -493,6 +508,35 @@ export function ActionAreaCard({ } } + async function handleMarkAccidentalReportConfirm() { + setIsMarkingAccidentalReport(true); + try { + const res = await fetch(`/api/phishing/campaigns/${campaignId}/mark-accidental-report`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(accidentalReportReason ? { reason: accidentalReportReason } : {}), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.message ?? data.error ?? 'Mark as accidental report failed'); + if (data.notePosted === false) { + toast.warning( + `Campaign marked as accidental report, but the reporter note failed to post${ + data.noteError ? `: ${data.noteError}` : '' + } — follow up manually.` + ); + } else { + toast.success('Marked as accidental report, reporter notified'); + } + setAccidentalReportDialogOpen(false); + setAccidentalReportReason(''); + onActionComplete(); + } catch (err) { + toast.error(`Mark as accidental report failed: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + setIsMarkingAccidentalReport(false); + } + } + return ( @@ -556,6 +600,16 @@ export function ActionAreaCard({ > Mark as false positive + + setAccidentalReportDialogOpen(true)} + > + Mark as accidental report + @@ -617,6 +671,38 @@ export function ActionAreaCard({ + + + + + Mark as an accidental report? + + Mark this campaign as an accidental report? This posts a note to the reporting employee + explaining no action is needed, and closes out the campaign. This cannot be undone. + + +
+ +