feat: wizard step indicator, wider dialog, campaigns in admin sidebar, clear leads shortcut

- Import wizard: added step indicator (numbered circles), widened to max-w-5xl
- Admin sidebar: added Marketing → Campaigns nav link
- Clear campaign leads: Ctrl+Shift+C shortcut with campaign picker modal (test-only)
- Test CSV data for all 3 campaigns
- Defect fixing plan + CSV import spec docs
- Session memory update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 12:45:05 +05:30
parent d9e2bedc1b
commit 7af1ccb713
9 changed files with 963 additions and 2 deletions

View File

@@ -0,0 +1,122 @@
import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrash } from '@fortawesome/pro-duotone-svg-icons';
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
import { Button } from '@/components/base/buttons/button';
import { Badge } from '@/components/base/badges/badges';
import { FeaturedIcon } from '@/components/foundations/featured-icon/featured-icon';
import { useData } from '@/providers/data-provider';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import { cx } from '@/utils/cx';
import type { FC } from 'react';
const TrashIcon: FC<{ className?: string }> = ({ className }) => (
<FontAwesomeIcon icon={faTrash} className={className} />
);
interface ClearCampaignLeadsModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}
export const ClearCampaignLeadsModal = ({ isOpen, onOpenChange }: ClearCampaignLeadsModalProps) => {
const { campaigns, leads, refresh } = useData();
const [selectedId, setSelectedId] = useState<string | null>(null);
const [clearing, setClearing] = useState(false);
const leadsPerCampaign = (campaignId: string) =>
leads.filter(l => l.campaignId === campaignId).length;
const handleClear = async () => {
if (!selectedId) return;
const campaignLeads = leads.filter(l => l.campaignId === selectedId);
if (campaignLeads.length === 0) {
notify.info('No Leads', 'No leads to clear for this campaign');
return;
}
setClearing(true);
let deleted = 0;
for (const lead of campaignLeads) {
try {
await apiClient.graphql(
`mutation($id: UUID!) { deleteLead(id: $id) { id } }`,
{ id: lead.id },
{ silent: true },
);
deleted++;
} catch {
// continue
}
}
notify.success('Leads Cleared', `${deleted} leads deleted from campaign`);
setClearing(false);
setSelectedId(null);
onOpenChange(false);
refresh();
};
const handleClose = () => {
if (clearing) return;
onOpenChange(false);
setSelectedId(null);
};
const selectedLeadCount = selectedId ? leadsPerCampaign(selectedId) : 0;
return (
<ModalOverlay isOpen={isOpen} onOpenChange={handleClose} isDismissable>
<Modal className="sm:max-w-md">
<Dialog>
{() => (
<div className="flex w-full flex-col rounded-xl bg-primary shadow-xl ring-1 ring-secondary overflow-hidden">
<div className="flex flex-col items-center gap-3 px-6 pt-6 pb-4">
<FeaturedIcon icon={TrashIcon} color="error" theme="light" size="md" />
<h2 className="text-lg font-semibold text-primary">Clear Campaign Leads</h2>
<p className="text-xs text-tertiary text-center">Select a campaign to delete all its imported leads. This cannot be undone.</p>
</div>
<div className="px-6 pb-4 space-y-2 max-h-60 overflow-y-auto">
{campaigns.map(c => {
const count = leadsPerCampaign(c.id);
return (
<button
key={c.id}
onClick={() => setSelectedId(c.id)}
className={cx(
'flex w-full items-center justify-between rounded-lg border-2 px-3 py-2.5 text-left transition duration-100 ease-linear',
selectedId === c.id ? 'border-error bg-error-primary' : 'border-secondary hover:border-error',
)}
>
<span className="text-sm font-medium text-primary">{c.campaignName ?? 'Untitled'}</span>
<Badge size="sm" color={count > 0 ? 'error' : 'gray'} type="pill-color">{count} leads</Badge>
</button>
);
})}
</div>
<div className="flex items-center gap-3 border-t border-secondary px-6 py-4">
<Button size="md" color="secondary" onClick={handleClose} className="flex-1" isDisabled={clearing}>
Cancel
</Button>
<Button
size="md"
color="primary-destructive"
onClick={handleClear}
isDisabled={!selectedId || selectedLeadCount === 0 || clearing}
isLoading={clearing}
className="flex-1"
>
{clearing ? 'Clearing...' : `Delete ${selectedLeadCount} Leads`}
</Button>
</div>
</div>
)}
</Dialog>
</Modal>
</ModalOverlay>
);
};