bolt-templates-sms-extensio.../client/src/pages/Businesses.jsx

768 lines
30 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 { fetchActiveSalesChannels } from '../utils/fyndSalesChannels';
import {
getApplicationId,
getBusinessDomain,
getBusinessImage,
getBusinessName,
getBusinessTagline,
} from '../utils/businessProfile';
function normalizeText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeUniqueStrings(value) {
if (!Array.isArray(value)) return [];
const seen = new Set();
return value
.map((entry) => normalizeText(entry))
.filter((entry) => {
if (!entry || seen.has(entry)) return false;
seen.add(entry);
return true;
});
}
function extractCdnUrls(business) {
return normalizeUniqueStrings(business?.relevantImagePaths);
}
function normalizeScrapeLinks(value) {
if (!Array.isArray(value)) return [];
const seen = new Set();
return value
.map((entry) => {
if (typeof entry === 'string') {
const href = normalizeText(entry);
return href ? { href, label: href } : null;
}
if (!entry || typeof entry !== 'object') return null;
const href = normalizeText(entry.href || entry.url || entry.link);
if (!href) return null;
const label = normalizeText(entry.text || entry.title || entry.label || href);
return { href, label };
})
.filter((entry) => {
if (!entry || seen.has(entry.href)) return false;
seen.add(entry.href);
return true;
});
}
function formatPrettyJson(value) {
if (value == null) return '';
if (typeof value === 'string') {
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch {
return value;
}
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function CdnGallery({ urls, compact = false, showLabels = true, clickable = true }) {
if (!urls.length) return null;
return (
<div className={`grid gap-3 ${compact ? 'grid-cols-2 sm:grid-cols-3' : 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3'}`}>
{urls.map((url, index) => {
const Wrapper = clickable ? 'a' : 'div';
const wrapperProps = clickable
? { href: url, target: '_blank', rel: 'noreferrer' }
: {};
return (
<Wrapper
key={`${url}-${index}`}
{...wrapperProps}
className={`group overflow-hidden rounded-xl border border-gray-200 bg-white transition ${clickable ? 'hover:border-primary-blue' : ''}`}
>
<div className={`bg-gray-50 ${compact ? 'aspect-[4/3]' : 'aspect-[5/4]'}`}>
<img
src={url}
alt={`Storefront image ${index + 1}`}
className="h-full w-full object-cover"
onError={(event) => {
event.currentTarget.style.opacity = '0.35';
}}
/>
</div>
{showLabels && (
<div className="border-t border-gray-100 px-3 py-2">
<p className="text-xs text-gray-500 break-all leading-relaxed line-clamp-3">{url}</p>
</div>
)}
</Wrapper>
);
})}
</div>
);
}
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 BusinessCreatedModal({ business, onClose }) {
const name = getBusinessName(business);
const domain = getBusinessDomain(business);
const tagline = getBusinessTagline(business);
const image = getBusinessImage(business);
const cdnUrls = extractCdnUrls(business?.scrapeArtifacts?.cdnUrls?.length ? { relevantImagePaths: business.scrapeArtifacts.cdnUrls } : business);
const links = normalizeScrapeLinks(business?.scrapeArtifacts?.links);
const prettyJson = useMemo(() => formatPrettyJson(business?.scrapeArtifacts?.json), [business]);
useEffect(() => {
const previousBodyOverflow = document.body.style.overflow;
const previousBodyOverscroll = document.body.style.overscrollBehavior;
const previousHtmlOverflow = document.documentElement.style.overflow;
const previousHtmlOverscroll = document.documentElement.style.overscrollBehavior;
document.body.style.overflow = 'hidden';
document.body.style.overscrollBehavior = 'none';
document.documentElement.style.overflow = 'hidden';
document.documentElement.style.overscrollBehavior = 'none';
return () => {
document.body.style.overflow = previousBodyOverflow;
document.body.style.overscrollBehavior = previousBodyOverscroll;
document.documentElement.style.overflow = previousHtmlOverflow;
document.documentElement.style.overscrollBehavior = previousHtmlOverscroll;
};
}, []);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/55 p-4 backdrop-blur-sm">
<div
className="flex h-full w-full flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl"
style={{ width: '920px', maxWidth: 'calc(100vw - 2rem)', height: 'min(78vh, 760px)' }}
>
<div className="shrink-0 flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-5">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Business created</p>
<h2 className="mt-2 text-2xl font-semibold text-gray-900">{name}</h2>
<p className="mt-1 text-sm text-gray-500">
{domain
? `Scrape completed for ${domain}. Review the captured assets below before moving on.`
: 'Scrape completed. Review the captured assets below before moving on.'}
</p>
</div>
<button
onClick={onClose}
className="rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-50 hover:text-gray-900"
>
Close
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-5">
<div className="space-y-5 pb-2">
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
<div className="flex items-start gap-4">
<div className="w-16 h-16 rounded-xl 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-xl font-bold text-primary-blue">{name?.[0]?.toUpperCase() || 'B'}</span>
)}
</div>
<div className="min-w-0">
<p className="text-lg font-semibold tracking-tight text-gray-900">{name}</p>
{domain && <p className="mt-1 text-sm font-medium text-gray-500 break-all">{domain}</p>}
{tagline && <p className="mt-2 text-sm leading-relaxed text-gray-700">{tagline}</p>}
<div className="mt-4 flex flex-wrap gap-2">
<span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-gray-600 border border-gray-200">
{cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'}
</span>
<span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-gray-600 border border-gray-200">
{links.length} link{links.length === 1 ? '' : 's'}
</span>
</div>
</div>
</div>
</div>
{cdnUrls.length > 0 && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Images</p>
<p className="mt-1 text-sm text-gray-500">Captured storefront images are available below.</p>
</div>
<CdnGallery urls={cdnUrls} compact showLabels={false} />
</section>
)}
{prettyJson && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Captured Data</p>
<p className="mt-1 text-sm text-gray-500">Raw storefront data captured during onboarding.</p>
</div>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-950">
<pre className="max-h-56 overflow-auto px-4 py-4 text-xs leading-relaxed text-gray-100">
{prettyJson}
</pre>
</div>
</section>
)}
{links.length > 0 && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Links</p>
<p className="mt-1 text-sm text-gray-500">Every discovered storefront link is available below.</p>
</div>
<div className="rounded-xl border border-gray-200">
<div className="max-h-72 overflow-y-auto divide-y divide-gray-100">
{links.map((link, index) => (
<a
key={`${link.href}-${index}`}
href={link.href}
target="_blank"
rel="noreferrer"
className="block px-4 py-3 transition hover:bg-gray-50"
>
<p className="text-sm font-medium text-gray-800 break-all">{link.label}</p>
<p className="mt-1 text-xs text-primary-blue break-all">{link.href}</p>
</a>
))}
</div>
</div>
</section>
)}
</div>
</div>
<div className="shrink-0 border-t border-gray-200 px-6 py-4">
<button
onClick={onClose}
className="w-full rounded-lg bg-primary-blue py-2 text-sm font-medium text-white transition hover:bg-primary-dark"
>
Continue
</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,
onSelect,
onImport,
onDelete,
onFallback,
}) {
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 cdnUrls = extractCdnUrls(item.business);
const isOpening = isScraped && selectingBusinessId === businessId;
const isImporting = !isScraped && creatingSalesChannelId === channelId;
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>
)}
{isScraped && cdnUrls.length > 0 && (
<div className="mt-4 border-t border-gray-100 pt-4">
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">Images</p>
<span className="text-xs font-medium text-gray-400">
{cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'}
</span>
</div>
<CdnGallery urls={cdnUrls.slice(0, 6)} compact showLabels={false} clickable={false} />
</div>
)}
</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={() => {
if (hasWebsiteUrl) {
onImport(item.channel);
return;
}
onFallback();
}}
disabled={isImporting}
className="text-sm text-primary-blue font-semibold group-hover:underline disabled:opacity-60"
>
{isImporting ? 'Scraping…' : hasWebsiteUrl ? 'Scrape →' : 'Use Fallback URL →'}
</button>
<span className="text-xs text-gray-500 font-medium">
{hasWebsiteUrl ? 'Ready to scrape' : '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 [showModal, setShowModal] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
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]);
async function handleBusinessCreated(created) {
setShowModal(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.');
}
}
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 res = await apiClient.post('/api/businesses', {
applicationId,
websiteUrl: channel.websiteUrl,
});
await handleBusinessCreated(res.data);
} 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);
}
}
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 scrape the ones that are not onboarded yet.'
: 'Add a storefront URL and well scrape it 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}
onSelect={handleSelect}
onImport={handleCreateFromSalesChannel}
onDelete={setDeleteTarget}
onFallback={() => setShowModal(true)}
/>
))}
</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}
onSelect={handleSelect}
onImport={handleCreateFromSalesChannel}
onDelete={setDeleteTarget}
onFallback={() => setShowModal(true)}
/>
))}
</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(); }}
onSuccess={handleBusinessCreated}
/>
)}
{createdBusiness && <BusinessCreatedModal business={createdBusiness} onClose={() => setCreatedBusiness(null)} />}
{deleteTarget && (
<DeleteConfirmModal
businessName={getBusinessName(deleteTarget)}
onCancel={() => setDeleteTarget(null)}
onConfirm={handleDelete}
deleting={deleting}
/>
)}
</div>
);
}