sms-extension-1777535448/client/src/pages/Businesses.jsx

748 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import apiClient from '../api/client';
import { useBusiness } from '../context/BusinessContext';
import RegisterBusinessModal from '../components/RegisterBusinessModal';
import BusinessReviewModal from '../components/BusinessReviewModal';
import { fetchActiveSalesChannels } from '../utils/fyndSalesChannels';
import {
fetchBusinessOnboardingJob,
getBusinessOnboardingError,
getBusinessOnboardingProgress,
getBusinessOnboardingStageMeta,
shouldRetryMissingBusinessOnboardingJob,
startBusinessOnboardingJob,
} from '../utils/businessOnboarding';
import {
getApplicationId,
getBusinessDomain,
getBusinessImage,
getBusinessName,
getBusinessTagline,
} from '../utils/businessProfile';
function normalizeText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function DeleteConfirmModal({ businessName, onCancel, onConfirm, deleting }) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 backdrop-blur-sm">
<div className="bg-white border border-gray-200 rounded-lg p-5 w-full max-w-md ">
<div className="w-12 h-12 rounded-full bg-white flex items-center justify-center mx-auto mb-4">
<span className="text-xl">🗑</span>
</div>
<h3 className="text-lg font-semibold text-gray-800 text-center mb-2">Delete Business?</h3>
<p className="text-sm text-gray-500 text-center mb-6">
This will permanently delete <span className="text-gray-800 font-medium">{businessName}</span> and all its events, templates, and images. This cannot be undone.
</p>
<div className="flex gap-3">
<button
onClick={onCancel}
disabled={deleting}
className="flex-1 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium transition disabled:opacity-50"
>
Cancel
</button>
<button
onClick={onConfirm}
disabled={deleting}
className="flex-1 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{deleting ? <><span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> Deleting</> : 'Yes, Delete'}
</button>
</div>
</div>
</div>
);
}
function BusinessOnboardingModal({ job, onClose }) {
const status = normalizeText(job?.status);
const stageMeta = getBusinessOnboardingStageMeta(job?.stage || status);
const progress = getBusinessOnboardingProgress(job);
const isFailed = status === 'failed';
const isCompleted = status === 'completed';
const discoveredPages = progress.pagesDiscovered;
const processedPages = progress.pagesProcessed;
const progressWidth = stageMeta.percent;
const errorMessage = getBusinessOnboardingError(job);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/55 p-4 backdrop-blur-sm">
<div className="w-full max-w-lg rounded-2xl border border-gray-200 bg-white shadow-xl">
<div className="border-b border-gray-200 px-6 py-5">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">
{isFailed ? 'Onboarding failed' : isCompleted ? 'Business ready' : 'Setting up business'}
</p>
<h2 className="mt-2 text-2xl font-semibold text-gray-900">{stageMeta.label}</h2>
<p className="mt-1 text-sm text-gray-500">
{isFailed
? 'The onboarding job could not be completed. You can close this dialog and try again.'
: isCompleted
? 'The storefront crawl and brand analysis finished successfully.'
: stageMeta.note}
</p>
</div>
<div className="space-y-5 px-6 py-5">
{!isFailed && (
<>
<div className="space-y-2">
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className={`h-full rounded-full transition-all ${isCompleted ? 'bg-green-500' : 'bg-primary-blue'}`}
style={{ width: `${progressWidth}%` }}
/>
</div>
<div className="flex items-center justify-between text-xs font-medium text-gray-500">
<span>Status: {status || 'pending'}</span>
{discoveredPages > 0 ? (
<span>{processedPages} / {discoveredPages} pages</span>
) : (
<span>Preparing crawl</span>
)}
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-gray-400">Pages</p>
<p className="mt-2 text-lg font-semibold text-gray-900">{processedPages}</p>
</div>
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-gray-400">Links</p>
<p className="mt-2 text-lg font-semibold text-gray-900">{progress.linkCount}</p>
</div>
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-gray-400">Images</p>
<p className="mt-2 text-lg font-semibold text-gray-900">{progress.imageCount}</p>
</div>
</div>
</>
)}
{isFailed && (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{errorMessage || 'Business onboarding failed.'}
</div>
)}
</div>
<div className="border-t border-gray-200 px-6 py-4">
{isFailed ? (
<button
onClick={onClose}
className="w-full rounded-lg bg-primary-blue py-2 text-sm font-medium text-white transition hover:bg-primary-dark"
>
Close
</button>
) : (
<button
disabled
className="w-full rounded-lg bg-gray-100 py-2 text-sm font-medium text-gray-500"
>
Working
</button>
)}
</div>
</div>
</div>
);
}
function StatusBadge({ status }) {
const isScraped = status === 'scraped';
return (
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold ${isScraped
? 'bg-green-100 text-green-700'
: 'bg-amber-100 text-amber-700'
}`}
>
<span
className={`h-2 w-2 rounded-full ${isScraped ? 'bg-green-500' : 'bg-amber-500'
}`}
/>
{isScraped ? 'Scraped' : 'Not Scraped Yet'}
</span>
);
}
function UnifiedBusinessCard({
item,
selectingBusinessId,
creatingSalesChannelId,
reviewLoadingBusinessId,
onSelect,
onImport,
onDelete,
onFallback,
onReview,
}) {
const entity = item.business || item.channel;
const businessId = item.business?.businessId || '';
const channelId = getApplicationId(item.channel);
const image = getBusinessImage(entity);
const name = getBusinessName(entity);
const domain = getBusinessDomain(entity);
const tagline = getBusinessTagline(entity);
const isScraped = item.status === 'scraped';
const isOpening = isScraped && selectingBusinessId === businessId;
const isImporting = !isScraped && creatingSalesChannelId === channelId;
const isLoadingReview = isScraped && reviewLoadingBusinessId === businessId;
const hasWebsiteUrl = Boolean(item.channel?.websiteUrl);
const canOpenBusiness = isScraped && item.business && !isOpening;
function handleCardClick() {
if (!canOpenBusiness) return;
onSelect(item.business);
}
function handleCardKeyDown(event) {
if (!canOpenBusiness) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onSelect(item.business);
}
}
return (
<div
className={`group rounded-lg bg-white border border-gray-200 transition-all overflow-hidden ${isScraped ? 'cursor-pointer hover:border-primary-blue hover:shadow-sm' : 'hover:border-primary-blue'}`}
onClick={handleCardClick}
onKeyDown={handleCardKeyDown}
role={isScraped ? 'button' : undefined}
tabIndex={isScraped ? 0 : undefined}
aria-label={isScraped ? `Open ${name}` : undefined}
>
<div className="p-5">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-4 min-w-0">
<div className="w-16 h-16 rounded-lg overflow-hidden bg-white border border-gray-200 shrink-0 flex items-center justify-center">
{image ? (
<img src={image} alt={name} className="w-full h-full object-cover" />
) : (
<span className="text-lg font-bold text-primary-blue">
{name?.[0]?.toUpperCase() || 'B'}
</span>
)}
</div>
<div className="min-w-0">
<p className="font-bold text-gray-800 truncate">{name}</p>
{domain && (
<p className="text-xs text-gray-500 font-medium truncate mt-1">{domain}</p>
)}
{tagline && (
<p className="text-sm text-gray-600 mt-3 leading-relaxed line-clamp-2">{tagline}</p>
)}
</div>
</div>
<StatusBadge status={item.status} />
</div>
{isScraped && item.business?.createdAt && (
<p className="text-xs text-gray-400 font-medium mt-4">
Added {new Date(item.business.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
</p>
)}
{!isScraped && !hasWebsiteUrl && (
<p className="text-xs text-amber-700 font-medium mt-4">
A website URL could not be derived automatically for this sales channel.
</p>
)}
</div>
<div className="px-5 py-3 bg-white border-t border-gray-100 flex items-center justify-between gap-3">
{isScraped ? (
<>
<button
onClick={(event) => {
event.stopPropagation();
onDelete(item.business);
}}
className="rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 transition hover:bg-red-50 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-200"
>
Delete
</button>
<button
onClick={(event) => {
event.stopPropagation();
onReview(item.business);
}}
disabled={isLoadingReview}
className="rounded-md bg-primary-blue px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-primary-blue/30 disabled:opacity-60"
>
{isLoadingReview ? 'Loading review…' : 'View Brand Review'}
</button>
</>
) : (
<>
<button
onClick={() => {
if (hasWebsiteUrl) {
onImport(item.channel);
return;
}
onFallback();
}}
disabled={isImporting}
className="text-sm text-primary-blue font-semibold group-hover:underline disabled:opacity-60"
>
{isImporting ? 'Onboarding…' : hasWebsiteUrl ? 'Start onboarding →' : 'Use fallback URL →'}
</button>
<span className="text-xs text-gray-500 font-medium">
{hasWebsiteUrl ? 'Ready to onboard' : 'Needs manual URL'}
</span>
</>
)}
</div>
</div>
);
}
export default function Businesses() {
const navigate = useNavigate();
const { setActiveBusiness } = useBusiness();
const [businesses, setBusinesses] = useState([]);
const [salesChannels, setSalesChannels] = useState([]);
const [loading, setLoading] = useState(true);
const [salesChannelsStatus, setSalesChannelsStatus] = useState('loading');
const [salesChannelQuery, setSalesChannelQuery] = useState('');
const [selectingBusinessId, setSelectingBusinessId] = useState('');
const [creatingSalesChannelId, setCreatingSalesChannelId] = useState('');
const [createdBusiness, setCreatedBusiness] = useState(null);
const [onboardingJob, setOnboardingJob] = useState(null);
const [showOnboardingModal, setShowOnboardingModal] = useState(false);
const [showModal, setShowModal] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
const [reviewBusiness, setReviewBusiness] = useState(null);
const [reviewLoadingBusinessId, setReviewLoadingBusinessId] = useState('');
const onboardingJobCreatedAt = onboardingJob?.createdAt;
const showUnifiedSalesChannelView = salesChannelsStatus === 'success';
const unifiedEntries = useMemo(() => {
const matchedBusinessIds = new Set();
const businessByApplicationId = new Map(
businesses
.map((business) => [getApplicationId(business), business])
.filter(([applicationId]) => Boolean(applicationId))
);
const mergedEntries = salesChannels.map((channel, index) => {
const applicationId = getApplicationId(channel);
const business = applicationId ? businessByApplicationId.get(applicationId) || null : null;
if (business?.businessId) {
matchedBusinessIds.add(business.businessId);
}
return {
key: `channel:${applicationId || channel.name || channel.domain || index}`,
status: business ? 'scraped' : 'not_scraped',
applicationId,
business,
channel,
};
});
const standaloneBusinesses = businesses
.filter((business) => !matchedBusinessIds.has(business.businessId))
.map((business) => ({
key: `business:${business.businessId}`,
status: 'scraped',
applicationId: getApplicationId(business),
business,
channel: null,
}));
return [...mergedEntries, ...standaloneBusinesses].sort((left, right) => {
if (left.status !== right.status) {
return left.status === 'not_scraped' ? -1 : 1;
}
return getBusinessName(left.business || left.channel)
.localeCompare(getBusinessName(right.business || right.channel));
});
}, [businesses, salesChannels]);
const filteredUnifiedEntries = useMemo(() => {
const query = salesChannelQuery.trim().toLowerCase();
if (!query) return unifiedEntries;
return unifiedEntries.filter((entry) => {
const entity = entry.business || entry.channel;
const name = String(getBusinessName(entity) || '').toLowerCase();
const domain = String(getBusinessDomain(entity) || '').toLowerCase();
const tagline = String(getBusinessTagline(entity) || '').toLowerCase();
return name.includes(query) || domain.includes(query) || tagline.includes(query);
});
}, [unifiedEntries, salesChannelQuery]);
const loadBusinesses = useCallback(async () => {
const res = await apiClient.get('/api/businesses');
setBusinesses(res.data.businesses || []);
}, []);
const loadSalesChannels = useCallback(async () => {
setSalesChannelsStatus('loading');
const channels = await fetchActiveSalesChannels();
setSalesChannels(channels);
setSalesChannelsStatus('success');
}, []);
const load = useCallback(async () => {
setLoading(true);
setError('');
try {
const [businessesRes, salesChannelsRes] = await Promise.allSettled([
loadBusinesses(),
loadSalesChannels(),
]);
if (businessesRes.status === 'rejected') {
setError('Failed to load businesses');
}
if (salesChannelsRes.status === 'rejected') {
setSalesChannels([]);
setSalesChannelsStatus('error');
}
} finally {
setLoading(false);
}
}, [loadBusinesses, loadSalesChannels]);
useEffect(() => { load(); }, [load]);
const handleBusinessCreated = useCallback(async (created) => {
setShowModal(false);
setShowOnboardingModal(false);
setCreatedBusiness(created);
try {
await Promise.all([loadBusinesses(), loadSalesChannels()]);
setSalesChannelsStatus('success');
} catch (err) {
setError(err.response?.data?.error || 'Business was created, but the business list could not be refreshed.');
}
}, [loadBusinesses, loadSalesChannels]);
const handleBusinessJobStarted = useCallback(async (job) => {
setError('');
setShowModal(false);
setCreatedBusiness(null);
setOnboardingJob(job);
setShowOnboardingModal(true);
}, []);
useEffect(() => {
if (!onboardingJob?.jobId) return undefined;
if (onboardingJob.status === 'completed') return undefined;
let cancelled = false;
let timeoutId = null;
let transientNotFoundMisses = 0;
async function pollJob() {
try {
const nextJob = await fetchBusinessOnboardingJob(onboardingJob.jobId);
if (cancelled) return;
transientNotFoundMisses = 0;
setOnboardingJob(nextJob);
if (nextJob.status === 'completed' && nextJob.business) {
await handleBusinessCreated(nextJob.business);
if (!cancelled) {
setOnboardingJob(null);
setShowOnboardingModal(false);
}
return;
}
if (nextJob.status === 'failed') {
setShowOnboardingModal(true);
return;
}
timeoutId = window.setTimeout(pollJob, 2200);
} catch (err) {
if (cancelled) return;
if (shouldRetryMissingBusinessOnboardingJob({ createdAt: onboardingJobCreatedAt }, err, transientNotFoundMisses)) {
transientNotFoundMisses += 1;
timeoutId = window.setTimeout(pollJob, 1800);
return;
}
setOnboardingJob((current) => ({
...(current || {}),
status: 'failed',
stage: 'failed',
error: { message: err.response?.data?.error || 'Failed to fetch onboarding progress.' },
}));
setShowOnboardingModal(true);
}
}
timeoutId = window.setTimeout(pollJob, 2500);
return () => {
cancelled = true;
if (timeoutId) window.clearTimeout(timeoutId);
};
}, [handleBusinessCreated, onboardingJob?.jobId, onboardingJob?.status, onboardingJobCreatedAt]);
async function handleSelect(biz) {
setSelectingBusinessId(biz.businessId);
setError('');
try {
const setupComplete = await setActiveBusiness(biz);
navigate(`/${biz.businessId}/${setupComplete ? 'events' : 'global-sms'}`);
} catch (err) {
setError(err.response?.data?.error || 'Failed to open business');
} finally {
setSelectingBusinessId('');
}
}
async function handleCreateFromSalesChannel(channel) {
const applicationId = getApplicationId(channel);
if (!applicationId) return;
if (!channel.websiteUrl) {
setError('A website URL could not be derived from this sales channel. Please use the fallback URL flow to continue.');
return;
}
setCreatingSalesChannelId(applicationId);
setError('');
try {
const job = await startBusinessOnboardingJob({
applicationId,
websiteUrl: channel.websiteUrl,
});
await handleBusinessJobStarted(job);
} catch (err) {
setError(err.response?.data?.error || 'Failed to add business from sales channel');
} finally {
setCreatingSalesChannelId('');
}
}
async function handleDelete() {
if (!deleteTarget) return;
setDeleting(true);
try {
await apiClient.delete(`/api/businesses/${deleteTarget.businessId}`);
setDeleteTarget(null);
await load();
} catch (err) {
setError(err.response?.data?.error || 'Failed to delete business');
} finally {
setDeleting(false);
}
}
async function handleOpenReview(business) {
if (!business?.businessId || reviewLoadingBusinessId) return;
setReviewLoadingBusinessId(business.businessId);
setError('');
try {
if (business?.scrapeArtifacts?.json) {
setReviewBusiness(business);
return;
}
const response = await apiClient.get(`/api/businesses/${business.businessId}`);
setReviewBusiness(response.data);
} catch (err) {
setError(err.response?.data?.error || 'Failed to load brand review.');
} finally {
setReviewLoadingBusinessId('');
}
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="w-8 h-8 border-2 border-gray-200 border-t-indigo-600 rounded-full animate-spin" />
</div>
);
}
return (
<div className="min-h-screen bg-white p-5">
<div className="max-w-5xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-800 tracking-tight">
{showUnifiedSalesChannelView ? 'Your Sales Channels' : (businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business')}
</h1>
<p className="text-sm text-gray-500 mt-1">
{showUnifiedSalesChannelView
? 'View every connected sales channel in one place and onboard the ones that are not scraped yet.'
: 'Add a storefront URL and well scrape the homepage, about page, and representative product pages to set up your business.'}
</p>
</div>
{!showUnifiedSalesChannelView && (
<button
onClick={() => setShowModal(true)}
className="px-4 py-2 rounded-lg bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold transition"
>
+ Add Business
</button>
)}
</div>
{error && (
<div className="mb-6 px-4 py-2 rounded-md bg-white border border-gray-200 text-gray-700 font-medium text-sm flex items-center justify-between">
{error}
<button onClick={() => setError('')} className="text-gray-600 hover:text-gray-700 font-bold">&times;</button>
</div>
)}
{showUnifiedSalesChannelView ? (
<section>
<div className="mb-4">
<h2 className="text-lg font-bold text-gray-800 tracking-tight">Sales Channels</h2>
<p className="text-sm text-gray-500 mt-1">Scraped businesses and active sales channels are shown together here.</p>
</div>
{unifiedEntries.length > 0 ? (
<div className="rounded-lg border border-gray-200 bg-white p-5">
<div className="mb-4">
<label className="block text-sm font-semibold text-gray-700 mb-1.5">Search Sales Channels</label>
<input
type="text"
value={salesChannelQuery}
onChange={(e) => setSalesChannelQuery(e.target.value)}
placeholder="Search by name, domain, or description"
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-800 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{filteredUnifiedEntries.map((item) => (
<UnifiedBusinessCard
key={item.key}
item={item}
selectingBusinessId={selectingBusinessId}
creatingSalesChannelId={creatingSalesChannelId}
reviewLoadingBusinessId={reviewLoadingBusinessId}
onSelect={handleSelect}
onImport={handleCreateFromSalesChannel}
onDelete={setDeleteTarget}
onFallback={() => setShowModal(true)}
onReview={handleOpenReview}
/>
))}
</div>
{filteredUnifiedEntries.length === 0 && (
<div className="mt-4 rounded-lg border border-dashed border-gray-300 bg-white p-5 text-center">
<p className="text-sm font-semibold text-gray-800">No sales channels matched your search.</p>
<p className="text-sm text-gray-500 mt-1">Try a different name or domain.</p>
</div>
)}
</div>
) : (
<div className="rounded-lg border border-dashed border-gray-300 bg-white p-5 text-center">
<p className="text-gray-800 font-semibold mb-1">No sales channels are available yet.</p>
<p className="text-sm text-gray-500 mb-4">Use the manual fallback only if you need to set up a storefront URL directly.</p>
<button
onClick={() => setShowModal(true)}
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium transition"
>
Use website URL fallback
</button>
</div>
)}
</section>
) : (
<section className="mb-10">
<div className="mb-4">
<h2 className="text-lg font-bold text-gray-800 tracking-tight">Configured Businesses</h2>
<p className="text-sm text-gray-500 mt-1">Select a business to manage its SMS templates.</p>
</div>
{businesses.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{businesses.map((biz) => (
<UnifiedBusinessCard
key={`fallback:${biz.businessId}`}
item={{ key: `fallback:${biz.businessId}`, status: 'scraped', business: biz, channel: null }}
selectingBusinessId={selectingBusinessId}
creatingSalesChannelId={creatingSalesChannelId}
reviewLoadingBusinessId={reviewLoadingBusinessId}
onSelect={handleSelect}
onImport={handleCreateFromSalesChannel}
onDelete={setDeleteTarget}
onFallback={() => setShowModal(true)}
onReview={handleOpenReview}
/>
))}
</div>
) : (
<div className="rounded-lg border border-dashed border-gray-300 bg-white p-5 text-center">
<p className="text-gray-800 font-semibold mb-1">No configured businesses yet.</p>
<p className="text-sm text-gray-500">Use Add Business to enter a storefront URL and get started.</p>
</div>
)}
</section>
)}
</div>
{showModal && (
<RegisterBusinessModal
onClose={() => { setShowModal(false); load(); }}
onJobStarted={handleBusinessJobStarted}
/>
)}
{onboardingJob && showOnboardingModal && (
<BusinessOnboardingModal
job={onboardingJob}
onClose={() => setShowOnboardingModal(false)}
/>
)}
{createdBusiness && (
<BusinessReviewModal
business={createdBusiness}
eyebrow="Business created"
helperText={createdBusiness?.domain
? `Onboarding completed for ${createdBusiness.domain}. Review the captured brand context before moving on.`
: 'Onboarding completed. Review the captured brand context before moving on.'}
closeLabel="Continue"
onClose={() => setCreatedBusiness(null)}
/>
)}
{reviewBusiness && (
<BusinessReviewModal
business={reviewBusiness}
onClose={() => setReviewBusiness(null)}
/>
)}
{deleteTarget && (
<DeleteConfirmModal
businessName={getBusinessName(deleteTarget)}
onCancel={() => setDeleteTarget(null)}
onConfirm={handleDelete}
deleting={deleting}
/>
)}
</div>
);
}