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 (
{urls.map((url, index) => { const Wrapper = clickable ? 'a' : 'div'; const wrapperProps = clickable ? { href: url, target: '_blank', rel: 'noreferrer' } : {}; return (
{`Storefront { event.currentTarget.style.opacity = '0.35'; }} />
{showLabels && (

{url}

)}
); })}
); } function DeleteConfirmModal({ businessName, onCancel, onConfirm, deleting }) { return (
đź—‘

Delete Business?

This will permanently delete {businessName} and all its events, templates, and images. This cannot be undone.

); } 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 (

Business created

{name}

{domain ? `Scrape completed for ${domain}. Review the captured assets below before moving on.` : 'Scrape completed. Review the captured assets below before moving on.'}

{image ? ( {name} ) : ( {name?.[0]?.toUpperCase() || 'B'} )}

{name}

{domain &&

{domain}

} {tagline &&

{tagline}

}
{cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'} {links.length} link{links.length === 1 ? '' : 's'}
{cdnUrls.length > 0 && (

Images

Captured storefront images are available below.

)} {prettyJson && (

Captured Data

Raw storefront data captured during onboarding.

                  {prettyJson}
                
)} {links.length > 0 && (

Links

Every discovered storefront link is available below.

{links.map((link, index) => (

{link.label}

{link.href}

))}
)}
); } function StatusBadge({ status }) { const isScraped = status === 'scraped'; return ( {isScraped ? 'Scraped' : 'Not Scraped Yet'} ); } 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 (
{image ? ( {name} ) : ( {name?.[0]?.toUpperCase() || 'B'} )}

{name}

{domain && (

{domain}

)} {tagline && (

{tagline}

)}
{isScraped && item.business?.createdAt && (

Added {new Date(item.business.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}

)} {!isScraped && !hasWebsiteUrl && (

A website URL could not be derived automatically for this sales channel.

)} {isScraped && cdnUrls.length > 0 && (

Images

{cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'}
)}
{isScraped ? ( <> ) : ( <> {hasWebsiteUrl ? 'Ready to scrape' : 'Needs manual URL'} )}
); } 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 (
); } return (

{showUnifiedSalesChannelView ? 'Your Sales Channels' : (businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business')}

{showUnifiedSalesChannelView ? 'View every connected sales channel in one place and scrape the ones that are not onboarded yet.' : 'Add a storefront URL and we’ll scrape it to set up your business.'}

{!showUnifiedSalesChannelView && ( )}
{error && (
{error}
)} {showUnifiedSalesChannelView ? (

Sales Channels

Scraped businesses and active sales channels are shown together here.

{unifiedEntries.length > 0 ? (
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" />
{filteredUnifiedEntries.map((item) => ( setShowModal(true)} /> ))}
{filteredUnifiedEntries.length === 0 && (

No sales channels matched your search.

Try a different name or domain.

)}
) : (

No sales channels are available yet.

Use the manual fallback only if you need to set up a storefront URL directly.

)}
) : (

Configured Businesses

Select a business to manage its SMS templates.

{businesses.length > 0 ? (
{businesses.map((biz) => ( setShowModal(true)} /> ))}
) : (

No configured businesses yet.

Use Add Business to enter a storefront URL and get started.

)}
)}
{showModal && ( { setShowModal(false); load(); }} onSuccess={handleBusinessCreated} /> )} {createdBusiness && setCreatedBusiness(null)} />} {deleteTarget && ( setDeleteTarget(null)} onConfirm={handleDelete} deleting={deleting} /> )}
); }