From c03fef4d95e8bdcebb2346918ef761b253f0c268 Mon Sep 17 00:00:00 2001 From: Ritul Date: Fri, 3 Apr 2026 15:54:45 +0530 Subject: [PATCH] Final fixes before migrating to openrouter key instead of workflows --- client/src/components/Sidebar.jsx | 68 +++- client/src/pages/GlobalSms.jsx | 25 +- client/src/pages/Providers.jsx | 627 +++++++++++++++++++++++++----- 3 files changed, 593 insertions(+), 127 deletions(-) diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx index ffe96f5..02eee4a 100644 --- a/client/src/components/Sidebar.jsx +++ b/client/src/components/Sidebar.jsx @@ -59,6 +59,26 @@ function StageMarker({ done, active, enabled }) { return ; } +function normalizeText(value) { + return typeof value === 'string' ? value.trim() : ''; +} + +function getSidebarBusinessImage(business) { + const brandingLogos = business?.scrapeArtifacts?.json?.branding?.logos; + const primaryLogo = Array.isArray(brandingLogos) + ? brandingLogos.find((entry) => normalizeText(entry)) + : ''; + if (primaryLogo) return primaryLogo; + + return ( + normalizeText(business?.logoUrl) + || normalizeText(business?.imageUrl) + || (Array.isArray(business?.relevantImagePaths) + ? business.relevantImagePaths.find((entry) => normalizeText(entry)) || '' + : '') + ); +} + export default function Sidebar({ onOpenReview, reviewLoading = false, reviewError = '' }) { const { activeBusiness, @@ -70,6 +90,7 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr } = useBusiness(); const navigate = useNavigate(); const location = useLocation(); + const businessImage = getSidebarBusinessImage(activeBusiness); const globalSmsPath = `/${activeBusinessId}/global-sms`; const eventsPath = `/${activeBusinessId}/events`; @@ -132,21 +153,30 @@ export default function Sidebar({ onOpenReview, reviewLoading = false, reviewErr Switch Business {activeBusiness && ( -
-
-
- {activeBusiness.brandName?.[0]?.toUpperCase() || 'B'} -
-
-

{activeBusiness.brandName}

-

{activeBusiness.domain}

- {activeBusinessId && ( - <> -
+
+
+ {businessImage ? ( + {activeBusiness.brandName + ) : ( + + {activeBusiness.brandName?.[0]?.toUpperCase() || 'B'} + + )} +
+
+

{activeBusiness.brandName}

+ {activeBusinessId && ( + <> +
+
- {reviewError && ( -

{reviewError}

- )} - - )} -
+
+ {reviewError && ( +

{reviewError}

+ )} + + )}
)} diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx index 9749a6f..40ee9f2 100644 --- a/client/src/pages/GlobalSms.jsx +++ b/client/src/pages/GlobalSms.jsx @@ -1,11 +1,12 @@ import { useState, useEffect, useCallback } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import apiClient from '../api/client'; import { useBusiness } from '../context/BusinessContext'; export default function GlobalSms() { const { businessId } = useParams(); const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness(); const [loading, setLoading] = useState(true); const [profiles, setProfiles] = useState([]); @@ -64,6 +65,28 @@ export default function GlobalSms() { loadProfiles(); }, [loadProfiles]); + useEffect(() => { + const editProfileId = searchParams.get('editProfile'); + if (!editProfileId || profiles.length === 0) return; + + const nextParams = new URLSearchParams(searchParams); + nextParams.delete('editProfile'); + + const matchingProfile = profiles.find((profile) => profile.id === editProfileId); + if (matchingProfile) { + setEditingId(matchingProfile.id); + setFormName(matchingProfile.name); + setFormCurl(matchingProfile.rawCurl); + setFormSetActive(false); + setError(''); + setSuccess(''); + } else { + setError('The requested profile could not be found.'); + } + + setSearchParams(nextParams, { replace: true }); + }, [profiles, searchParams, setSearchParams]); + const activeProfile = profiles.find(p => p.id === activeProfileId) || null; const hasProfiles = profiles.length > 0; const isCreatingFirstProfile = !hasProfiles && !editingId; diff --git a/client/src/pages/Providers.jsx b/client/src/pages/Providers.jsx index dce5f87..0bb1072 100644 --- a/client/src/pages/Providers.jsx +++ b/client/src/pages/Providers.jsx @@ -1,12 +1,76 @@ -import { useState, useEffect } from 'react'; -import { useParams } from 'react-router-dom'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import apiClient from '../api/client'; +import { useBusiness } from '../context/BusinessContext'; + +const DESKTOP_SPLIT_QUERY = '(min-width: 1100px)'; +const DEFAULT_LIST_PANE_WIDTH = 340; +const MIN_LIST_PANE_WIDTH = 280; +const MAX_LIST_PANE_WIDTH = 420; +const MIN_DETAIL_PANE_WIDTH = 440; + +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} + +function getMissingProviderFields(profile) { + const provider = profile?.provider || {}; + const missing = []; + + if (!provider.providerName) missing.push('Provider Name'); + if (!provider.senderId) missing.push('Sender ID'); + if (!provider.dltEntityId) missing.push('DLT Entity ID'); + + return missing; +} + +function isProviderSetupComplete(profile) { + return getMissingProviderFields(profile).length === 0; +} + +function formatUpdatedAt(value) { + if (!value) return 'Not updated yet'; + + try { + return new Date(value).toLocaleString(); + } catch { + return 'Not updated yet'; + } +} + +function buildProviderSummary(profile) { + const provider = profile?.provider || {}; + const parts = []; + + if (provider.providerName) parts.push(provider.providerName); + if (provider.senderId) parts.push(`Sender ${provider.senderId}`); + if (provider.dltEntityId) parts.push('DLT added'); + + return parts.length > 0 ? parts.join(' • ') : 'Provider details not completed yet'; +} + +function ProfileStatusPill({ complete }) { + return ( + + {complete ? 'Complete' : 'Missing Fields'} + + ); +} export default function Providers() { const { businessId } = useParams(); + const navigate = useNavigate(); + const { refreshOnboardingState } = useBusiness(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const [activeProfile, setActiveProfile] = useState(null); + const [profiles, setProfiles] = useState([]); + const [activeProfileId, setActiveProfileId] = useState(''); + const [selectedProfileId, setSelectedProfileId] = useState(''); const [form, setForm] = useState({ providerName: '', senderId: '', @@ -15,54 +79,192 @@ export default function Providers() { }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); + const [copiedProfileId, setCopiedProfileId] = useState(''); + const [isDesktopSplit, setIsDesktopSplit] = useState(false); + const [listPaneWidth, setListPaneWidth] = useState(DEFAULT_LIST_PANE_WIDTH); + const layoutRef = useRef(null); + const copyTimeoutRef = useRef(null); - useEffect(() => { - async function load() { - try { - const [activeRes, providerRes] = await Promise.all([ - apiClient.get(`/api/businesses/${businessId}/global-sms/active`), - apiClient.get(`/api/businesses/${businessId}/providers`), - ]); + const globalSmsPath = `/${businessId}/global-sms`; - setActiveProfile(activeRes.data?.activeProfile || null); - setForm({ - providerName: providerRes.data?.providerName || '', - senderId: providerRes.data?.senderId || '', - dltEntityId: providerRes.data?.dltEntityId || '', - authKey: providerRes.data?.authKey || '', - }); - } catch (err) { - setError(err.response?.data?.error || 'Failed to load provider configuration'); - } finally { - setLoading(false); - } + const loadProfiles = useCallback(async () => { + try { + setLoading(true); + const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`); + const fetchedProfiles = res.data?.profiles || []; + const nextActiveProfileId = String(res.data?.activeProfileId || ''); + + setProfiles(fetchedProfiles); + setActiveProfileId(nextActiveProfileId); + setSelectedProfileId((currentSelectedProfileId) => ( + fetchedProfiles.some((profile) => profile.id === currentSelectedProfileId) + ? currentSelectedProfileId + : '' + )); + } catch (err) { + setError(err.response?.data?.error || 'Failed to load provider profiles'); + } finally { + setLoading(false); } - load(); }, [businessId]); + useEffect(() => { + loadProfiles(); + }, [loadProfiles]); + + useEffect(() => { + const mediaQuery = window.matchMedia(DESKTOP_SPLIT_QUERY); + const syncLayoutMode = (event) => setIsDesktopSplit(event.matches); + + setIsDesktopSplit(mediaQuery.matches); + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncLayoutMode); + return () => mediaQuery.removeEventListener('change', syncLayoutMode); + } + + mediaQuery.addListener(syncLayoutMode); + return () => mediaQuery.removeListener(syncLayoutMode); + }, []); + + useEffect(() => () => { + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + } + }, []); + + const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) || null; + + useEffect(() => { + if (!selectedProfile) { + setForm({ + providerName: '', + senderId: '', + dltEntityId: '', + authKey: '', + }); + return; + } + + const provider = selectedProfile.provider || {}; + setForm({ + providerName: provider.providerName || '', + senderId: provider.senderId || '', + dltEntityId: provider.dltEntityId || '', + authKey: provider.authKey || '', + }); + }, [selectedProfile]); + function handleChange(field, value) { - setForm(prev => ({ ...prev, [field]: value })); + setForm((prev) => ({ ...prev, [field]: value })); } - async function handleSave(e) { - e.preventDefault(); + function handleSelectProfile(profileId) { + setSelectedProfileId(profileId); + setError(''); + setSuccess(''); + } + + function handleResizeStart(event) { + if (!isDesktopSplit) return; + + event.preventDefault(); + const containerBounds = layoutRef.current?.getBoundingClientRect(); + if (!containerBounds) return; + + const maxAllowedWidth = clamp( + containerBounds.width - MIN_DETAIL_PANE_WIDTH, + MIN_LIST_PANE_WIDTH, + MAX_LIST_PANE_WIDTH, + ); + + function handlePointerMove(moveEvent) { + const nextWidth = clamp( + moveEvent.clientX - containerBounds.left, + MIN_LIST_PANE_WIDTH, + maxAllowedWidth, + ); + setListPaneWidth(nextWidth); + } + + function handlePointerUp() { + document.body.style.userSelect = ''; + window.removeEventListener('mousemove', handlePointerMove); + window.removeEventListener('mouseup', handlePointerUp); + } + + document.body.style.userSelect = 'none'; + window.addEventListener('mousemove', handlePointerMove); + window.addEventListener('mouseup', handlePointerUp); + } + + async function handleActivate(profile) { + if (!profile?.id) return; + + try { + setError(''); + setSuccess(''); + await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${profile.id}/activate`); + setSelectedProfileId(profile.id); + await loadProfiles(); + await refreshOnboardingState(businessId).catch(() => null); + setSuccess(`${profile.name} is now the active profile.`); + } catch (err) { + setError(err.response?.data?.error || 'Failed to activate profile'); + } + } + + async function handleCopyCurl(profile) { + if (!profile?.rawCurl) return; + + try { + if (!navigator?.clipboard?.writeText) { + throw new Error('Clipboard API unavailable'); + } + + await navigator.clipboard.writeText(profile.rawCurl); + setCopiedProfileId(profile.id); + + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current); + } + + copyTimeoutRef.current = window.setTimeout(() => { + setCopiedProfileId(''); + }, 1800); + } catch { + setError('Failed to copy the cURL command.'); + } + } + + async function handleSave(event) { + event.preventDefault(); + + if (!selectedProfile?.id) return; + setSaving(true); setError(''); setSuccess(''); + if (form.senderId && !/^[A-Za-z]{6}$/.test(form.senderId)) { setError('DLT Sender ID must be exactly 6 alphabet characters'); setSaving(false); return; } + try { - const res = await apiClient.post(`/api/businesses/${businessId}/providers`, form); - setForm({ - providerName: res.data?.providerName || '', - senderId: res.data?.senderId || '', - dltEntityId: res.data?.dltEntityId || '', - authKey: res.data?.authKey || '', + await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${selectedProfile.id}`, { + provider: { + providerName: form.providerName, + senderId: form.senderId.toUpperCase(), + dltEntityId: form.dltEntityId, + authKey: form.authKey, + }, }); - setSuccess('Provider configuration saved successfully.'); + + await loadProfiles(); + await refreshOnboardingState(businessId).catch(() => null); + setSuccess(`Provider configuration saved for ${selectedProfile.name}.`); } catch (err) { setError(err.response?.data?.error || 'Failed to save configuration'); } finally { @@ -79,98 +281,309 @@ export default function Providers() { } return ( -
-
-

Provider Configuration

-

Edit the provider details stored on the active cURL profile.

- {activeProfile && ( -

- Active Profile: {activeProfile.name} +

+
+
+

Provider Configuration

+

+ Pick a saved profile to review its complete request and manage the provider details stored against it.

- )} +
+
{error && ( -
+
{error}
)} {success && ( -
+
{success}
)} -
-
-
- - handleChange('providerName', e.target.value)} - className={`w-full px-4 py-2 rounded-lg bg-surface-white border ${!form.providerName ? 'border-error-text focus:ring-error-text' : 'border-border-main focus:ring-primary-blue'} text-text-primary placeholder-placeholder-bg font-medium focus:outline-none focus:ring-2 focus:border-transparent transition text-sm `} - placeholder="e.g. MSG91, Gupshup" - /> -
- -
-
- - handleChange('senderId', e.target.value.toUpperCase())} - maxLength={6} - className={`w-full px-4 py-2 rounded-lg bg-surface-white border ${!form.senderId ? 'border-error-text focus:ring-error-text' : 'border-border-main focus:ring-primary-blue'} text-text-primary font-mono tracking-widest placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:border-transparent transition text-sm uppercase`} - placeholder="6 CHARS" - /> -

Exactly 6 alphabetic characters (e.g. MOKOBA).

-
- -
- - handleChange('dltEntityId', e.target.value)} - className={`w-full px-4 py-2 rounded-lg bg-surface-white border ${!form.dltEntityId ? 'border-error-text focus:ring-error-text' : 'border-border-main focus:ring-primary-blue'} text-text-primary font-mono placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:border-transparent transition text-sm `} - placeholder="19-digit DLT PE ID" - /> +
+
+
+
+
+

Saved Profiles

+
+ + {profiles.length} total +
-
- - handleChange('authKey', e.target.value)} - className="w-full px-4 py-2 rounded-lg bg-surface-white border border-border-main text-text-primary font-mono placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent transition text-sm " - placeholder="Authorization key for your SMS provider" - /> -

Used as the Authorization header in your SMS requests.

-
-
+ {profiles.length === 0 ? ( +
+

No saved profiles yet

+

+ Add and validate a cURL profile from Omni-channel SMS before configuring provider details here. +

+ +
+ ) : ( +
+
+ {profiles.map((profile) => { + const isActive = profile.id === activeProfileId; + const isSelected = profile.id === selectedProfileId; + const complete = isProviderSetupComplete(profile); -
- -
- + return ( + + ); + })} +
+
+ )} + + + {isDesktopSplit && ( +
+ +
+ )} + +
+ {!selectedProfile ? ( +
+
+

Select a profile

+

Choose a saved profile to review

+

+ The selected profile will open here with a complete cURL preview, provider details, and activation controls. +

+
+
+ ) : ( + <> +
+
+
+
+

{selectedProfile.name}

+ {selectedProfile.id === activeProfileId && ( + + Active profile + + )} + +
+

+ Review the exact saved request, then update the provider fields tied to this profile. +

+
+ +
+ {selectedProfile.id !== activeProfileId && ( + + )} + + +
+
+
+ +
+
+
+
+

Preview

+
+ + Updated {formatUpdatedAt(selectedProfile.updatedAt)} + +
+
+                    {selectedProfile.rawCurl}
+                  
+
+ +
+
+
+

Provider Details

+

+ These fields are stored against this profile and are used during template publishing. +

+
+ +
+
+ + handleChange('providerName', event.target.value)} + className={`w-full px-4 py-2 rounded-lg bg-surface-white border ${!form.providerName ? 'border-error-text focus:ring-error-text' : 'border-border-main focus:ring-primary-blue'} text-text-primary placeholder-placeholder-bg font-medium focus:outline-none focus:ring-2 focus:border-transparent transition text-sm`} + placeholder="e.g. MSG91, Gupshup" + /> +
+ +
+
+ + handleChange('senderId', event.target.value.toUpperCase())} + maxLength={6} + className={`w-full px-4 py-2 rounded-lg bg-surface-white border ${!form.senderId ? 'border-error-text focus:ring-error-text' : 'border-border-main focus:ring-primary-blue'} text-text-primary font-mono tracking-widest placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:border-transparent transition text-sm uppercase`} + placeholder="6 CHARS" + /> +

Exactly 6 alphabetic characters.

+
+ +
+ + handleChange('dltEntityId', event.target.value)} + className={`w-full px-4 py-2 rounded-lg bg-surface-white border ${!form.dltEntityId ? 'border-error-text focus:ring-error-text' : 'border-border-main focus:ring-primary-blue'} text-text-primary font-mono placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:border-transparent transition text-sm`} + placeholder="19-digit DLT PE ID" + /> +
+
+ +
+ + handleChange('authKey', event.target.value)} + className="w-full px-4 py-2 rounded-lg bg-surface-white border border-border-main text-text-primary font-mono placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue focus:border-transparent transition text-sm" + placeholder="Authorization key for your SMS provider" + /> +

Used as the Authorization header in your SMS requests.

+
+
+ +
+ +
+
+ + +
+
+ + )} +
+
); }