@@ -127,7 +127,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
)}
{error && (
-
+
{error}
)}
@@ -141,7 +141,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
type="text"
value={providerForm.providerName}
onChange={e => setProviderForm(prev => ({ ...prev, providerName: e.target.value }))}
- className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
+ className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
placeholder="e.g. MSG91"
autoFocus
required
@@ -156,7 +156,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
type="text"
value={providerForm.senderId}
onChange={e => setProviderForm(prev => ({ ...prev, senderId: e.target.value.toUpperCase() }))}
- className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono uppercase text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
+ className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono uppercase text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
placeholder="6 CHARS"
maxLength={6}
required
@@ -171,7 +171,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
type="text"
value={providerForm.dltEntityId}
onChange={e => setProviderForm(prev => ({ ...prev, dltEntityId: e.target.value }))}
- className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
+ className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
placeholder="19-digit DLT PE ID"
required
/>
@@ -183,7 +183,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
type="button"
onClick={onClose}
disabled={savingProvider}
- className="flex-1 py-2.5 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50"
+ className="flex-1 py-2 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50"
>
Cancel
@@ -195,7 +195,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
if (field === 'dltEntityId') return !providerForm.dltEntityId.trim();
return false;
})}
- className="flex-1 py-2.5 rounded-lg bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold transition shadow-sm disabled:opacity-50 flex items-center justify-center gap-2"
+ className="flex-1 py-2 rounded-lg bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{savingProvider ? <>
Saving…> : 'Save Details'}
@@ -210,7 +210,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
value={templateId}
onChange={e => setTemplateId(e.target.value)}
placeholder="e.g. 1234567890987654321"
- className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
+ className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
autoFocus
required
/>
@@ -223,7 +223,7 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
value={toNumber}
onChange={e => setToNumber(e.target.value)}
placeholder="e.g. 919876543210"
- className="w-full px-4 py-2.5 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
+ className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main font-mono text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue text-sm"
required
/>
This sends the publish-triggering SMS request.
@@ -234,14 +234,14 @@ export default function WhitelistModal({ businessId, template, boundProfile, onC
type="button"
onClick={onClose}
disabled={publishing}
- className="flex-1 py-2.5 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50"
+ className="flex-1 py-2 rounded-lg border border-border-main text-text-primary hover:bg-page-bg text-sm font-medium transition disabled:opacity-50"
>
Cancel
diff --git a/client/src/pages/Brand.jsx b/client/src/pages/Brand.jsx
index b87b026..d512498 100644
--- a/client/src/pages/Brand.jsx
+++ b/client/src/pages/Brand.jsx
@@ -13,26 +13,26 @@ const NAV_CARDS = [
function DeleteConfirmModal({ brandName, onCancel, onConfirm, deleting }) {
return (
-
-
+
+
🗑
-
Delete Brand?
+
Delete Brand?
- This will permanently delete {brandName} and all associated events, templates, and images. This cannot be undone.
+ This will permanently delete {brandName} and all associated events, templates, and images. This cannot be undone.
@@ -64,8 +64,8 @@ export default function Brand() {
if (loading) {
return (
-
-
+
);
}
@@ -73,18 +73,18 @@ export default function Brand() {
// — WELCOME SCREEN —
if (!brand) {
return (
-
+
-
+
S
-
SMS Template Extension
+
SMS Template Extension
Generate TRAI-compliant SMS templates for your Fynd store. We'll scrape your website and use AI to extract your brand context automatically.
@@ -96,17 +96,17 @@ export default function Brand() {
// — BRAND DETAIL PAGE —
return (
-
+
{/* Brand header card */}
-
+
-
{brand.brandName}
+
{brand.brandName}
{brand.domain}
-
+
{brand.tone}
@@ -116,7 +116,7 @@ export default function Brand() {
@@ -142,7 +142,7 @@ export default function Brand() {
{brand.colors.map((c, i) => (
@@ -156,11 +156,11 @@ export default function Brand() {
{/* Brand images */}
{brand.relevantImagePaths?.length > 0 && (
-
+
Brand Images
{brand.relevantImagePaths.map((url, i) => (
-
+
{card.icon}
-
{card.label}
+
{card.label}
{card.desc}
))}
diff --git a/client/src/pages/Businesses.jsx b/client/src/pages/Businesses.jsx
index b4a1d58..72e1664 100644
--- a/client/src/pages/Businesses.jsx
+++ b/client/src/pages/Businesses.jsx
@@ -5,36 +5,36 @@ import { useBusiness } from '../context/BusinessContext';
import RegisterBusinessModal from '../components/RegisterBusinessModal';
import { fetchActiveSalesChannels } from '../utils/fyndSalesChannels';
import {
+ getApplicationId,
getBusinessDomain,
getBusinessImage,
getBusinessName,
getBusinessTagline,
- getChannelId,
} from '../utils/businessProfile';
function DeleteConfirmModal({ businessName, onCancel, onConfirm, deleting }) {
return (
-
-
+
+
🗑
-
Delete Business?
+
Delete Business?
- This will permanently delete {businessName} and all its events, templates, and images. This cannot be undone.
+ This will permanently delete {businessName} and all its events, templates, and images. This cannot be undone.
@@ -52,13 +52,13 @@ function BusinessCreatedModal({ business, onClose }) {
return (
-
-
✓
-
Business Added!
+
+
✓
+
Business Added!
Your business is ready for onboarding.
-
+
-
+
{image ? (

) : (
@@ -74,7 +74,7 @@ function BusinessCreatedModal({ business, onClose }) {
@@ -83,37 +83,125 @@ function BusinessCreatedModal({ business, onClose }) {
);
}
-function SalesChannelCard({ channel, disabled, onImport }) {
- const name = getBusinessName(channel);
- const domain = getBusinessDomain(channel);
- const image = getBusinessImage(channel);
+function StatusBadge({ status }) {
+ const isScraped = status === 'scraped';
return (
-
-
-
- {image ? (
-

- ) : (
-
{name?.[0]?.toUpperCase() || 'S'}
- )}
-
-
-
{name}
-
{domain || 'Domain unavailable'}
+
+
+ {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 isOpening = isScraped && selectingBusinessId === businessId;
+ const isImporting = !isScraped && creatingSalesChannelId === channelId;
+ const hasWebsiteUrl = Boolean(item.channel?.websiteUrl);
+
+ return (
+
+
+
+
+
+ {image ? (
+

+ ) : (
+
+ {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.
+
+ )}
-
-
- {channel.websiteUrl ? 'Ready to scrape' : 'Use manual URL fallback'}
-
-
+
+
+ {isScraped ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ {hasWebsiteUrl ? 'Ready to scrape' : 'Needs manual URL'}
+
+ >
+ )}
);
@@ -135,29 +223,65 @@ export default function Businesses() {
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
- const configuredApplicationIds = useMemo(() => (
- new Set(
+ const showUnifiedSalesChannelView = salesChannelsStatus === 'success';
+
+ const unifiedEntries = useMemo(() => {
+ const matchedBusinessIds = new Set();
+ const businessByApplicationId = new Map(
businesses
- .map((business) => String(business?.applicationId || '').trim())
- .filter(Boolean)
- )
- ), [businesses]);
+ .map((business) => [getApplicationId(business), business])
+ .filter(([applicationId]) => Boolean(applicationId))
+ );
- const availableSalesChannels = useMemo(() => (
- salesChannels.filter((channel) => !configuredApplicationIds.has(getChannelId(channel)))
- ), [configuredApplicationIds, salesChannels]);
- const showSalesChannelsSection = salesChannelsStatus === 'success' && availableSalesChannels.length > 0;
+ const mergedEntries = salesChannels.map((channel, index) => {
+ const applicationId = getApplicationId(channel);
+ const business = applicationId ? businessByApplicationId.get(applicationId) || null : null;
- const filteredSalesChannels = useMemo(() => {
- const query = salesChannelQuery.trim().toLowerCase();
- if (!query) return availableSalesChannels;
+ if (business?.businessId) {
+ matchedBusinessIds.add(business.businessId);
+ }
- return availableSalesChannels.filter((channel) => {
- const name = String(getBusinessName(channel) || '').toLowerCase();
- const domain = String(getBusinessDomain(channel) || '').toLowerCase();
- return name.includes(query) || domain.includes(query);
+ return {
+ key: `channel:${applicationId || channel.name || channel.domain || index}`,
+ status: business ? 'scraped' : 'not_scraped',
+ applicationId,
+ business,
+ channel,
+ };
});
- }, [availableSalesChannels, salesChannelQuery]);
+
+ 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');
@@ -210,11 +334,11 @@ export default function Businesses() {
}
async function handleCreateFromSalesChannel(channel) {
- const applicationId = getChannelId(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 Add Business and enter the URL manually.');
+ setError('A website URL could not be derived from this sales channel. Please use the fallback URL flow to continue.');
return;
}
@@ -252,162 +376,132 @@ export default function Businesses() {
if (loading) {
return (
-
-
+
);
}
return (
-
+
-
- {businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business'}
+
+ {showUnifiedSalesChannelView ? 'Your Sales Channels' : (businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business')}
- {showSalesChannelsSection
- ? 'Import from an active sales channel when available, or use the website URL fallback to scrape manually.'
+ {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}
-
+
)}
-
-
-
Configured Businesses
-
Select a business to manage its SMS templates.
-
-
- {businesses.length > 0 ? (
-
- {businesses.map((biz) => (
-
-
-
- Click to manage →
-
-
-
- ))}
-
- ) : (
-
-
No configured businesses yet.
-
Use Add Business to enter a storefront URL and get started.
-
- )}
-
-
- {showSalesChannelsSection && (
-
-
-
-
Active Sales Channels
-
These are pulled directly from Commerce and can be scraped into businesses with one click.
-
-
-
-
+ {showUnifiedSalesChannelView ? (
+
-
-
setSalesChannelQuery(e.target.value)}
- placeholder="Search by channel name or domain"
- className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm text-gray-900 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent"
- />
+
Sales Channels
+
Scraped businesses and active sales channels are shown together here.
-
- {filteredSalesChannels.map((channel) => {
- const channelId = getChannelId(channel);
- return (
-
handleCreateFromSalesChannel(channel)}
+ {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"
/>
- );
- })}
-
+
- {filteredSalesChannels.length === 0 && (
-
-
No active sales channels matched your search.
-
Use the website URL fallback if you want to scrape a storefront directly.
+
+ {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); loadBusinesses(); }} />
+ { setShowModal(false); load(); }} />
)}
{createdBusiness && setCreatedBusiness(null)} />}
{deleteTarget && (
diff --git a/client/src/pages/Events.jsx b/client/src/pages/Events.jsx
index 43d7cdb..9cc12a9 100644
--- a/client/src/pages/Events.jsx
+++ b/client/src/pages/Events.jsx
@@ -102,18 +102,18 @@ const DEFAULT_EXPANDED_GROUPS = EVENT_GROUPS.reduce((acc, group) => {
const EVENT_TEMPLATE_STATUS_CONFIG = {
unselected: {
label: 'No template selected',
- wrapper: 'border-gray-200 bg-gray-50 text-gray-500',
+ wrapper: 'border-gray-200 bg-white text-gray-500',
dot: 'bg-gray-400',
},
pending_whitelisting: {
label: 'Pending Whitelisting',
- wrapper: 'border-amber-200 bg-amber-50 text-amber-700',
- dot: 'bg-amber-500',
+ wrapper: 'border-gray-200 bg-white text-gray-700',
+ dot: 'bg-white0',
},
whitelisted: {
label: 'Published',
- wrapper: 'border-green-200 bg-green-50 text-green-700',
- dot: 'bg-green-500',
+ wrapper: 'border-gray-200 bg-white text-gray-700',
+ dot: 'bg-white0',
},
};
@@ -507,7 +507,7 @@ export default function Events() {
if (loading) {
return (
);
}
@@ -519,7 +519,7 @@ export default function Events() {
-
Events
+
Events
Generate SMS templates for each order event.
@@ -533,7 +533,7 @@ export default function Events() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search events"
- className="w-full rounded-lg border border-gray-300 bg-white py-3 pl-11 pr-10 text-sm font-medium text-gray-900 placeholder-gray-400 shadow-sm transition focus:border-primary-blue focus:outline-none focus:ring-2 focus:ring-indigo-100"
+ className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-11 pr-10 text-sm font-medium text-gray-800 placeholder-gray-400 transition focus:border-primary-blue focus:outline-none focus:ring-2 focus:ring-indigo-100"
/>
{searchTerm && (