diff --git a/client/src/App.jsx b/client/src/App.jsx index 0767099..7847e48 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -36,7 +36,7 @@ function SubLayout({ children }) { // Guard: redirect to / if no active business in session. // Also enforce cURL-first: only the cURL profile route is available until an active profile exists. function BusinessGuard({ children, isGlobalSmsRoute }) { - const { activeBusinessId, loading, hasGlobalSms } = useBusiness(); + const { activeBusinessId, loading, isSetupComplete } = useBusiness(); const location = useLocation(); if (loading) { @@ -51,7 +51,7 @@ function BusinessGuard({ children, isGlobalSmsRoute }) { return ; } - if (!hasGlobalSms && !isGlobalSmsRoute) { + if (!isSetupComplete && !isGlobalSmsRoute) { return ; } diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx index 5bac79d..3bfcd00 100644 --- a/client/src/components/Sidebar.jsx +++ b/client/src/components/Sidebar.jsx @@ -29,9 +29,9 @@ export default function Sidebar() { const navigate = useNavigate(); const navItems = [ - { id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Global SMS cURL' }, - { id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' }, - { id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' }, + { id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Omni-channel SMS' }, + { id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' }, + { id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' }, ]; function handleSwitch() { @@ -70,10 +70,9 @@ export default function Sidebar() { key={id} to={to} className={({ isActive }) => - `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${ - isActive - ? 'bg-refresh-hover text-primary-blue' - : 'text-text-muted hover:text-text-primary hover:bg-row-hover' + `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${isActive + ? 'bg-refresh-hover text-primary-blue' + : 'text-text-muted hover:text-text-primary hover:bg-row-hover' }` } > diff --git a/client/src/context/BusinessContext.jsx b/client/src/context/BusinessContext.jsx index 6ffe7f9..64bc56a 100644 --- a/client/src/context/BusinessContext.jsx +++ b/client/src/context/BusinessContext.jsx @@ -10,8 +10,16 @@ const SESSION_KEY = 'sms_active_business'; export function BusinessProvider({ children }) { const [activeBusiness, setActiveBusinessState] = useState(null); const [hasGlobalSms, setHasGlobalSms] = useState(false); + const [isSetupComplete, setIsSetupComplete] = useState(false); const [loading, setLoading] = useState(true); + const updateReadyState = useCallback((activeProfile) => { + const hasProfile = !!activeProfile; + setHasGlobalSms(hasProfile); + const p = activeProfile?.provider || {}; + setIsSetupComplete(hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId); + }, []); + // On mount: rehydrate from sessionStorage and refresh from API useEffect(() => { async function rehydrate() { @@ -30,7 +38,7 @@ export function BusinessProvider({ children }) { apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} })) ]); setActiveBusinessState(bizRes.data); - setHasGlobalSms(!!smsRes.data?.activeProfile); + updateReadyState(smsRes.data?.activeProfile); sessionStorage.setItem(SESSION_KEY, JSON.stringify({ businessId, companyId: runtimeCompanyId || companyId || '', @@ -40,6 +48,7 @@ export function BusinessProvider({ children }) { sessionStorage.removeItem(SESSION_KEY); setActiveBusinessState(null); setHasGlobalSms(false); + setIsSetupComplete(false); } finally { setLoading(false); } @@ -55,15 +64,17 @@ export function BusinessProvider({ children }) { })); try { const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`); - setHasGlobalSms(!!smsRes.data?.activeProfile); + updateReadyState(smsRes.data?.activeProfile); } catch { setHasGlobalSms(false); + setIsSetupComplete(false); } }, []); const clearBusiness = useCallback(() => { setActiveBusinessState(null); setHasGlobalSms(false); + setIsSetupComplete(false); sessionStorage.removeItem(SESSION_KEY); }, []); @@ -71,7 +82,7 @@ export function BusinessProvider({ children }) { return ( {children} diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx index 61fdde0..9467f42 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 } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import apiClient from '../api/client'; import { useBusiness } from '../context/BusinessContext'; export default function GlobalSms() { const { businessId } = useParams(); - const { setHasGlobalSms } = useBusiness(); + const navigate = useNavigate(); + const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness(); const [loading, setLoading] = useState(true); const [profiles, setProfiles] = useState([]); const [activeProfileId, setActiveProfileId] = useState(null); @@ -14,30 +15,56 @@ export default function GlobalSms() { const [error, setError] = useState(''); const [success, setSuccess] = useState(''); - // Form state for Create / Edit + // Form state for Create / Edit Profile const [editingId, setEditingId] = useState(null); const [formName, setFormName] = useState(''); const [formCurl, setFormCurl] = useState(''); const [formSetActive, setFormSetActive] = useState(true); + // Form state for Missing Provider Fields + const [providerForm, setProviderForm] = useState({ providerName: '', senderId: '', dltEntityId: '' }); + const [savingProvider, setSavingProvider] = useState(false); + const loadProfiles = useCallback(async () => { try { setLoading(true); const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`); - setProfiles(res.data.profiles || []); - setActiveProfileId(res.data.activeProfileId); - setHasGlobalSms(!!res.data.activeProfileId); + const fetchedProfiles = res.data.profiles || []; + const fetchActiveId = res.data.activeProfileId; + setProfiles(fetchedProfiles); + setActiveProfileId(fetchActiveId); + + const activeProfile = fetchedProfiles.find(p => p.id === fetchActiveId) || null; + const hasProfile = !!activeProfile; + setHasGlobalSms(hasProfile); + + const p = activeProfile?.provider || {}; + const complete = hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId; + setIsSetupComplete(complete); + + setProviderForm({ + providerName: p.providerName || '', + senderId: p.senderId || '', + dltEntityId: p.dltEntityId || '', + }); } catch { setError('Failed to load cURL profiles'); } finally { setLoading(false); } - }, [businessId, setHasGlobalSms]); + }, [businessId, setHasGlobalSms, setIsSetupComplete]); useEffect(() => { loadProfiles(); }, [loadProfiles]); + const activeProfile = profiles.find(p => p.id === activeProfileId) || null; + const pData = activeProfile?.provider || {}; + const missingFields = []; + if (activeProfile && !pData.providerName) missingFields.push('providerName'); + if (activeProfile && !pData.senderId) missingFields.push('senderId'); + if (activeProfile && !pData.dltEntityId) missingFields.push('dltEntityId'); + function handleAddClick() { setEditingId(null); setFormName(''); @@ -51,7 +78,7 @@ export default function GlobalSms() { setEditingId(profile.id); setFormName(profile.name); setFormCurl(profile.rawCurl); - setFormSetActive(false); // only matters for create + setFormSetActive(false); setError(''); setSuccess(''); } @@ -108,6 +135,30 @@ export default function GlobalSms() { } } + async function handleProviderSubmit(e) { + e.preventDefault(); + if (!activeProfileId) return; + setSavingProvider(true); + setError(''); + setSuccess(''); + + try { + await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, { + provider: { + providerName: providerForm.providerName, + senderId: providerForm.senderId.toUpperCase(), + dltEntityId: providerForm.dltEntityId, + } + }); + setSuccess('Provider details saved successfully!'); + await loadProfiles(); + } catch (err) { + setError(err.response?.data?.error || 'Failed to save provider details'); + } finally { + setSavingProvider(false); + } + } + if (loading) { return (
@@ -118,12 +169,37 @@ export default function GlobalSms() { return (
- {/* Header */} + {/* Header & Stepper */}
-

cURL Profiles

-

- Manage the cURL commands used to generate and test SMS templates. The active profile will be used across the application. +

Setup configuration

+

+ Complete this flow to configure your cURL profile and brand provider data. You must finish setup before generating templates.

+ + {/* CSS Stepper */} +
+ {[ + { label: 'Add / Select Profile', done: !!activeProfile, active: !activeProfile }, + { label: 'Validate cURL', done: !!activeProfile, active: false }, + { label: 'Complete Fields', done: isSetupComplete, active: !!activeProfile && !isSetupComplete }, + { label: 'Ready', done: isSetupComplete, active: isSetupComplete } + ].map((step, idx) => ( +
+
+ {step.done ? '✓' : idx + 1} +
+ + {step.label} + +
+ ))} +
{error && ( @@ -139,8 +215,105 @@ export default function GlobalSms() {
)} + {/* Active Profile Setup Review Block */} + {activeProfile && ( +
+
+

Active Setup: {activeProfile.name}

+ {isSetupComplete ? ( + Setup Complete + ) : ( + Missing Information + )} +
+ +
+
+

Parsed Provider Data:

+
    +
  • + Provider: + {pData.providerName || Missing} +
  • +
  • + Sender ID: + {pData.senderId || Missing} +
  • +
  • + Entity ID: + {pData.dltEntityId || Missing} +
  • +
  • + Auth Key: + {pData.authKey ? '••••••••' : 'None setup'} +
  • +
+
+ + {!isSetupComplete && ( +
+

Please fill in the missing fields:

+
+ {missingFields.includes('providerName') && ( + setProviderForm({ ...providerForm, providerName: e.target.value })} + className="w-full px-3 py-2 border border-border-main rounded text-sm focus:ring-1 focus:ring-primary-blue bg-page-bg" + required + /> + )} + {missingFields.includes('senderId') && ( + setProviderForm({ ...providerForm, senderId: e.target.value.toUpperCase() })} + className="w-full px-3 py-2 border border-border-main rounded text-sm focus:ring-1 focus:ring-primary-blue bg-page-bg uppercase" + required + /> + )} + {missingFields.includes('dltEntityId') && ( + setProviderForm({ ...providerForm, dltEntityId: e.target.value })} + className="w-full px-3 py-2 border border-border-main rounded text-sm focus:ring-1 focus:ring-primary-blue bg-page-bg" + required + /> + )} + +
+
+ )} + + {isSetupComplete && ( +
+

Your active cURL profile is fully configured.

+ +
+ )} +
+
+ )} + {/* Profiles List */} -
+
+

All Profiles

{profiles.length > 0 ? ( profiles.map(p => { const isActive = p.id === activeProfileId;