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 (
đź—‘

Delete Business?

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

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

{isFailed ? 'Onboarding failed' : isCompleted ? 'Business ready' : 'Setting up business'}

{stageMeta.label}

{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}

{!isFailed && ( <>
Status: {status || 'pending'} {discoveredPages > 0 ? ( {processedPages} / {discoveredPages} pages ) : ( Preparing crawl )}

Pages

{processedPages}

Links

{progress.linkCount}

Images

{progress.imageCount}

)} {isFailed && (
{errorMessage || 'Business onboarding failed.'}
)}
{isFailed ? ( ) : ( )}
); } function StatusBadge({ status }) { const isScraped = status === 'scraped'; return ( {isScraped ? 'Scraped' : 'Not Scraped Yet'} ); } 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 (
{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 ? ( <> ) : ( <> {hasWebsiteUrl ? 'Ready to onboard' : '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 [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 (
); } 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 onboard the ones that are not scraped yet.' : 'Add a storefront URL and we’ll scrape the homepage, about page, and representative product pages 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)} onReview={handleOpenReview} /> ))}
{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)} onReview={handleOpenReview} /> ))}
) : (

No configured businesses yet.

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

)}
)}
{showModal && ( { setShowModal(false); load(); }} onJobStarted={handleBusinessJobStarted} /> )} {onboardingJob && showOnboardingModal && ( setShowOnboardingModal(false)} /> )} {createdBusiness && ( setCreatedBusiness(null)} /> )} {reviewBusiness && ( setReviewBusiness(null)} /> )} {deleteTarget && ( setDeleteTarget(null)} onConfirm={handleDelete} deleting={deleting} /> )}
); }