Stepper for the entire process instead of just provider config
This commit is contained in:
parent
b34915c58b
commit
2785f5ee96
|
|
@ -1,12 +1,7 @@
|
||||||
import { NavLink, useNavigate } from 'react-router-dom';
|
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useBusiness } from '../context/BusinessContext';
|
import { useBusiness } from '../context/BusinessContext';
|
||||||
|
|
||||||
const SVG_ICONS = {
|
const SVG_ICONS = {
|
||||||
providers: (
|
|
||||||
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
globalSms: (
|
globalSms: (
|
||||||
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
|
@ -24,14 +19,75 @@ const SVG_ICONS = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Sidebar() {
|
function TopLevelStatus({ done, active }) {
|
||||||
const { activeBusiness, activeBusinessId, clearBusiness } = useBusiness();
|
if (done) {
|
||||||
const navigate = useNavigate();
|
return (
|
||||||
|
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-indigo-600 text-white shadow-sm">
|
||||||
|
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const navItems = [
|
if (active) {
|
||||||
{ id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Omni-channel SMS' },
|
return <span className="inline-block h-2.5 w-2.5 rounded-full bg-indigo-600 shadow-sm" />;
|
||||||
{ id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' },
|
}
|
||||||
{ id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' },
|
|
||||||
|
return <span className="inline-block h-2.5 w-2.5 rounded-full bg-gray-200" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const {
|
||||||
|
activeBusiness,
|
||||||
|
activeBusinessId,
|
||||||
|
clearBusiness,
|
||||||
|
hasGlobalSms,
|
||||||
|
isSetupComplete,
|
||||||
|
hasSelectedTemplates,
|
||||||
|
} = useBusiness();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const globalSmsPath = `/${activeBusinessId}/global-sms`;
|
||||||
|
const eventsPath = `/${activeBusinessId}/events`;
|
||||||
|
const templatesPath = `/${activeBusinessId}/templates`;
|
||||||
|
|
||||||
|
const isGlobalSmsRoute = location.pathname === globalSmsPath;
|
||||||
|
const isEventsRoute = location.pathname === eventsPath;
|
||||||
|
const isTemplatesRoute = location.pathname === templatesPath;
|
||||||
|
|
||||||
|
const omniSubsteps = [
|
||||||
|
{ id: 'profile', label: 'Add / Select Profile', done: hasGlobalSms, active: isGlobalSmsRoute && !hasGlobalSms },
|
||||||
|
{ id: 'validate', label: 'Validate cURL', done: hasGlobalSms, active: false },
|
||||||
|
{ id: 'fields', label: 'Complete Fields', done: isSetupComplete, active: isGlobalSmsRoute && hasGlobalSms && !isSetupComplete },
|
||||||
|
{ id: 'ready', label: 'Ready', done: isSetupComplete, active: isGlobalSmsRoute && isSetupComplete },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepItems = [
|
||||||
|
{
|
||||||
|
id: 'globalSms',
|
||||||
|
to: globalSmsPath,
|
||||||
|
label: 'Omni-channel SMS',
|
||||||
|
done: isSetupComplete && !isGlobalSmsRoute,
|
||||||
|
active: isGlobalSmsRoute,
|
||||||
|
expanded: isGlobalSmsRoute,
|
||||||
|
substeps: omniSubsteps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'events',
|
||||||
|
to: eventsPath,
|
||||||
|
label: 'Events',
|
||||||
|
done: hasSelectedTemplates && !isEventsRoute,
|
||||||
|
active: isEventsRoute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'templates',
|
||||||
|
to: templatesPath,
|
||||||
|
label: 'Templates',
|
||||||
|
done: false,
|
||||||
|
active: isTemplatesRoute,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleSwitch() {
|
function handleSwitch() {
|
||||||
|
|
@ -64,22 +120,65 @@ export default function Sidebar() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav */}
|
{/* Nav */}
|
||||||
<nav className="flex-1 px-3 pt-5 space-y-1">
|
<nav className="flex-1 px-3 pt-5">
|
||||||
{navItems.map(({ id, to, label }) => (
|
<div className="space-y-2">
|
||||||
<NavLink
|
{stepItems.map((item) => (
|
||||||
key={id}
|
<div key={item.id}>
|
||||||
to={to}
|
<NavLink
|
||||||
className={({ isActive }) =>
|
to={item.to}
|
||||||
`flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${isActive
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-semibold transition-colors duration-150 ${
|
||||||
? 'bg-refresh-hover text-primary-blue'
|
item.active
|
||||||
: 'text-text-muted hover:text-text-primary hover:bg-row-hover'
|
? 'bg-refresh-hover text-primary-blue'
|
||||||
}`
|
: 'text-text-muted hover:text-text-primary hover:bg-row-hover'
|
||||||
}
|
}`}
|
||||||
>
|
>
|
||||||
{SVG_ICONS[id]}
|
{SVG_ICONS[item.id]}
|
||||||
{label}
|
<span className="flex-1 truncate">{item.label}</span>
|
||||||
</NavLink>
|
<div className="flex items-center gap-3">
|
||||||
))}
|
<TopLevelStatus done={item.done} active={item.active} />
|
||||||
|
{item.substeps && (
|
||||||
|
<svg
|
||||||
|
className={`h-4 w-4 text-gray-400 transition-transform ${item.expanded ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
{item.expanded && item.substeps && (
|
||||||
|
<div className="relative ml-8 mt-2 space-y-2 pl-6 before:absolute before:left-[5px] before:top-2 before:bottom-3 before:w-px before:bg-indigo-100">
|
||||||
|
{item.substeps.map((substep) => (
|
||||||
|
<div
|
||||||
|
key={substep.id}
|
||||||
|
className={`relative rounded-xl px-3 py-2 text-sm font-medium transition-colors ${
|
||||||
|
substep.active
|
||||||
|
? 'bg-indigo-50 text-indigo-700 shadow-sm'
|
||||||
|
: substep.done
|
||||||
|
? 'text-gray-700'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute -left-[24px] top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full border-2 ${
|
||||||
|
substep.active
|
||||||
|
? 'border-indigo-100 bg-indigo-600'
|
||||||
|
: substep.done
|
||||||
|
? 'border-indigo-300 bg-white'
|
||||||
|
: 'border-gray-300 bg-white'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{substep.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="px-5 py-4 border-t border-gray-100">
|
<div className="px-5 py-4 border-t border-gray-100">
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,49 @@ export function BusinessProvider({ children }) {
|
||||||
const [activeBusiness, setActiveBusinessState] = useState(null);
|
const [activeBusiness, setActiveBusinessState] = useState(null);
|
||||||
const [hasGlobalSms, setHasGlobalSms] = useState(false);
|
const [hasGlobalSms, setHasGlobalSms] = useState(false);
|
||||||
const [isSetupComplete, setIsSetupComplete] = useState(false);
|
const [isSetupComplete, setIsSetupComplete] = useState(false);
|
||||||
|
const [hasSelectedTemplates, setHasSelectedTemplates] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const updateReadyState = useCallback((activeProfile) => {
|
const updateReadyState = useCallback((activeProfile, templates = []) => {
|
||||||
const hasProfile = !!activeProfile;
|
const hasProfile = !!activeProfile;
|
||||||
setHasGlobalSms(hasProfile);
|
setHasGlobalSms(hasProfile);
|
||||||
const p = activeProfile?.provider || {};
|
const p = activeProfile?.provider || {};
|
||||||
const nextIsSetupComplete = hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId;
|
const nextIsSetupComplete = hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId;
|
||||||
setIsSetupComplete(nextIsSetupComplete);
|
setIsSetupComplete(nextIsSetupComplete);
|
||||||
return nextIsSetupComplete;
|
const nextHasSelectedTemplates = Array.isArray(templates)
|
||||||
|
? templates.some((template) => !!template?.selectedTemplate)
|
||||||
|
: false;
|
||||||
|
setHasSelectedTemplates(nextHasSelectedTemplates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasGlobalSms: hasProfile,
|
||||||
|
isSetupComplete: nextIsSetupComplete,
|
||||||
|
hasSelectedTemplates: nextHasSelectedTemplates,
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const refreshOnboardingState = useCallback(async (businessIdOverride) => {
|
||||||
|
const targetBusinessId = businessIdOverride || activeBusiness?.businessId;
|
||||||
|
|
||||||
|
if (!targetBusinessId) {
|
||||||
|
setHasGlobalSms(false);
|
||||||
|
setIsSetupComplete(false);
|
||||||
|
setHasSelectedTemplates(false);
|
||||||
|
return {
|
||||||
|
hasGlobalSms: false,
|
||||||
|
isSetupComplete: false,
|
||||||
|
hasSelectedTemplates: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [smsRes, templatesRes] = await Promise.all([
|
||||||
|
apiClient.get(`/api/businesses/${targetBusinessId}/global-sms/active`).catch(() => ({ data: {} })),
|
||||||
|
apiClient.get(`/api/businesses/${targetBusinessId}/templates`).catch(() => ({ data: { templates: [] } })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return updateReadyState(smsRes.data?.activeProfile, templatesRes.data?.templates || []);
|
||||||
|
}, [activeBusiness?.businessId, updateReadyState]);
|
||||||
|
|
||||||
// On mount: rehydrate from sessionStorage and refresh from API
|
// On mount: rehydrate from sessionStorage and refresh from API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function rehydrate() {
|
async function rehydrate() {
|
||||||
|
|
@ -37,10 +69,13 @@ export function BusinessProvider({ children }) {
|
||||||
|
|
||||||
const [bizRes, smsRes] = await Promise.all([
|
const [bizRes, smsRes] = await Promise.all([
|
||||||
apiClient.get(`/api/businesses/${businessId}`),
|
apiClient.get(`/api/businesses/${businessId}`),
|
||||||
apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} }))
|
Promise.all([
|
||||||
|
apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} })),
|
||||||
|
apiClient.get(`/api/businesses/${businessId}/templates`).catch(() => ({ data: { templates: [] } })),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
setActiveBusinessState(bizRes.data);
|
setActiveBusinessState(bizRes.data);
|
||||||
updateReadyState(smsRes.data?.activeProfile);
|
updateReadyState(smsRes[0].data?.activeProfile, smsRes[1].data?.templates || []);
|
||||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
|
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
|
||||||
businessId,
|
businessId,
|
||||||
companyId: runtimeCompanyId || companyId || '',
|
companyId: runtimeCompanyId || companyId || '',
|
||||||
|
|
@ -51,6 +86,7 @@ export function BusinessProvider({ children }) {
|
||||||
setActiveBusinessState(null);
|
setActiveBusinessState(null);
|
||||||
setHasGlobalSms(false);
|
setHasGlobalSms(false);
|
||||||
setIsSetupComplete(false);
|
setIsSetupComplete(false);
|
||||||
|
setHasSelectedTemplates(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -65,19 +101,21 @@ export function BusinessProvider({ children }) {
|
||||||
companyId: getRuntimeCompanyId(),
|
companyId: getRuntimeCompanyId(),
|
||||||
}));
|
}));
|
||||||
try {
|
try {
|
||||||
const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`);
|
const progress = await refreshOnboardingState(business.businessId);
|
||||||
return updateReadyState(smsRes.data?.activeProfile);
|
return progress.isSetupComplete;
|
||||||
} catch {
|
} catch {
|
||||||
setHasGlobalSms(false);
|
setHasGlobalSms(false);
|
||||||
setIsSetupComplete(false);
|
setIsSetupComplete(false);
|
||||||
|
setHasSelectedTemplates(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [updateReadyState]);
|
}, [refreshOnboardingState]);
|
||||||
|
|
||||||
const clearBusiness = useCallback(() => {
|
const clearBusiness = useCallback(() => {
|
||||||
setActiveBusinessState(null);
|
setActiveBusinessState(null);
|
||||||
setHasGlobalSms(false);
|
setHasGlobalSms(false);
|
||||||
setIsSetupComplete(false);
|
setIsSetupComplete(false);
|
||||||
|
setHasSelectedTemplates(false);
|
||||||
sessionStorage.removeItem(SESSION_KEY);
|
sessionStorage.removeItem(SESSION_KEY);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -85,7 +123,18 @@ export function BusinessProvider({ children }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BusinessContext.Provider value={{
|
<BusinessContext.Provider value={{
|
||||||
activeBusiness, activeBusinessId, setActiveBusiness, clearBusiness, loading, hasGlobalSms, setHasGlobalSms, isSetupComplete, setIsSetupComplete
|
activeBusiness,
|
||||||
|
activeBusinessId,
|
||||||
|
setActiveBusiness,
|
||||||
|
clearBusiness,
|
||||||
|
loading,
|
||||||
|
hasGlobalSms,
|
||||||
|
setHasGlobalSms,
|
||||||
|
isSetupComplete,
|
||||||
|
setIsSetupComplete,
|
||||||
|
hasSelectedTemplates,
|
||||||
|
setHasSelectedTemplates,
|
||||||
|
refreshOnboardingState,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</BusinessContext.Provider>
|
</BusinessContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import apiClient from '../api/client';
|
import apiClient from '../api/client';
|
||||||
|
import { useBusiness } from '../context/BusinessContext';
|
||||||
|
|
||||||
const MAX_SMS_LENGTH = 160;
|
const MAX_SMS_LENGTH = 160;
|
||||||
const DLT_VARIABLE_OPTIONS = [
|
const DLT_VARIABLE_OPTIONS = [
|
||||||
|
|
@ -99,6 +100,8 @@ function buildTemplateUiState(templates = []) {
|
||||||
|
|
||||||
export default function Events() {
|
export default function Events() {
|
||||||
const { businessId } = useParams();
|
const { businessId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { hasSelectedTemplates, refreshOnboardingState } = useBusiness();
|
||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [newLabel, setNewLabel] = useState('');
|
const [newLabel, setNewLabel] = useState('');
|
||||||
|
|
@ -259,16 +262,21 @@ export default function Events() {
|
||||||
|
|
||||||
async function handleSelect(slug, variant, variantIndex) {
|
async function handleSelect(slug, variant, variantIndex) {
|
||||||
const variantKey = getVariantKey(slug, variantIndex);
|
const variantKey = getVariantKey(slug, variantIndex);
|
||||||
|
const shouldAutoAdvance = !hasSelectedTemplates;
|
||||||
setSelectingVariantKey(variantKey);
|
setSelectingVariantKey(variantKey);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant });
|
await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant });
|
||||||
|
await refreshOnboardingState(businessId).catch(() => null);
|
||||||
setVariants((currentVariants) => ({ ...currentVariants, [slug]: [] }));
|
setVariants((currentVariants) => ({ ...currentVariants, [slug]: [] }));
|
||||||
setVariantDrafts((currentDrafts) => removeDraftsForSlug(currentDrafts, slug));
|
setVariantDrafts((currentDrafts) => removeDraftsForSlug(currentDrafts, slug));
|
||||||
setOpenVariableMenuKey('');
|
setOpenVariableMenuKey('');
|
||||||
setActiveCaretVariantKey('');
|
setActiveCaretVariantKey('');
|
||||||
setGenState((state) => ({ ...state, [slug]: 'selected' }));
|
setGenState((state) => ({ ...state, [slug]: 'selected' }));
|
||||||
|
if (shouldAutoAdvance) {
|
||||||
|
navigate(`/${businessId}/templates`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || 'Failed to select template');
|
setError(err.response?.data?.error || 'Failed to select template');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export default function GlobalSms() {
|
||||||
// Form state for Missing Provider Fields
|
// Form state for Missing Provider Fields
|
||||||
const [providerForm, setProviderForm] = useState({ providerName: '', senderId: '', dltEntityId: '' });
|
const [providerForm, setProviderForm] = useState({ providerName: '', senderId: '', dltEntityId: '' });
|
||||||
const [savingProvider, setSavingProvider] = useState(false);
|
const [savingProvider, setSavingProvider] = useState(false);
|
||||||
|
const eventsPath = `/${businessId}/events`;
|
||||||
|
|
||||||
const loadProfiles = useCallback(async () => {
|
const loadProfiles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -47,8 +48,13 @@ export default function GlobalSms() {
|
||||||
senderId: p.senderId || '',
|
senderId: p.senderId || '',
|
||||||
dltEntityId: p.dltEntityId || '',
|
dltEntityId: p.dltEntityId || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { activeProfile, hasProfile, complete };
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to load cURL profiles');
|
setError('Failed to load cURL profiles');
|
||||||
|
setHasGlobalSms(false);
|
||||||
|
setIsSetupComplete(false);
|
||||||
|
return { activeProfile: null, hasProfile: false, complete: false };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +95,7 @@ export default function GlobalSms() {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
|
const shouldAutoAdvance = !isSetupComplete;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
|
|
@ -105,10 +112,13 @@ export default function GlobalSms() {
|
||||||
});
|
});
|
||||||
setSuccess('Profile created successfully.');
|
setSuccess('Profile created successfully.');
|
||||||
}
|
}
|
||||||
await loadProfiles();
|
const nextState = await loadProfiles();
|
||||||
setFormName('');
|
setFormName('');
|
||||||
setFormCurl('');
|
setFormCurl('');
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
|
if (shouldAutoAdvance && nextState.complete) {
|
||||||
|
navigate(eventsPath);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || 'Failed to save cURL profile');
|
setError(err.response?.data?.error || 'Failed to save cURL profile');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -127,9 +137,13 @@ export default function GlobalSms() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleActivate(id) {
|
async function handleActivate(id) {
|
||||||
|
const shouldAutoAdvance = !isSetupComplete;
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${id}/activate`);
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${id}/activate`);
|
||||||
await loadProfiles();
|
const nextState = await loadProfiles();
|
||||||
|
if (shouldAutoAdvance && nextState.complete) {
|
||||||
|
navigate(eventsPath);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || 'Failed to activate profile');
|
setError(err.response?.data?.error || 'Failed to activate profile');
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +155,7 @@ export default function GlobalSms() {
|
||||||
setSavingProvider(true);
|
setSavingProvider(true);
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
|
const shouldAutoAdvance = !isSetupComplete;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, {
|
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, {
|
||||||
|
|
@ -151,7 +166,10 @@ export default function GlobalSms() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setSuccess('Provider details saved successfully!');
|
setSuccess('Provider details saved successfully!');
|
||||||
await loadProfiles();
|
const nextState = await loadProfiles();
|
||||||
|
if (shouldAutoAdvance && nextState.complete) {
|
||||||
|
navigate(eventsPath);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || 'Failed to save provider details');
|
setError(err.response?.data?.error || 'Failed to save provider details');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -169,37 +187,11 @@ export default function GlobalSms() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto space-y-8 pb-12">
|
<div className="max-w-4xl mx-auto space-y-8 pb-12">
|
||||||
{/* Header & Stepper */}
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-text-primary mb-2">Setup configuration</h2>
|
<h2 className="text-2xl font-bold text-text-primary mb-2">Omni-channel SMS</h2>
|
||||||
<p className="text-sm text-text-muted mb-8">
|
<p className="text-sm text-text-muted">
|
||||||
Complete this flow to configure your cURL profile and brand provider data. You must finish setup before generating templates.
|
Complete this flow to configure your cURL profile and brand provider data. You must finish setup before generating templates.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* CSS Stepper */}
|
|
||||||
<div className="flex items-center w-full justify-between relative before:absolute before:top-4 before:left-0 before:h-[2px] before:w-full before:bg-border-soft before:-z-10">
|
|
||||||
{[
|
|
||||||
{ 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) => (
|
|
||||||
<div key={idx} className="flex flex-col items-center">
|
|
||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors border-2 ${step.done
|
|
||||||
? 'bg-primary-blue border-primary-blue text-white'
|
|
||||||
: step.active
|
|
||||||
? 'bg-white border-primary-blue text-primary-blue'
|
|
||||||
: 'bg-white border-border-main text-text-muted'
|
|
||||||
}`}>
|
|
||||||
{step.done ? '✓' : idx + 1}
|
|
||||||
</div>
|
|
||||||
<span className={`mt-2 text-[11px] uppercase tracking-wide font-bold ${step.done || step.active ? 'text-primary-blue' : 'text-text-muted'
|
|
||||||
}`}>
|
|
||||||
{step.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -300,7 +292,7 @@ export default function GlobalSms() {
|
||||||
<div className="flex flex-col justify-center items-center h-full space-y-4">
|
<div className="flex flex-col justify-center items-center h-full space-y-4">
|
||||||
<p className="text-center text-sm font-medium text-text-muted">Your active cURL profile is fully configured.</p>
|
<p className="text-center text-sm font-medium text-text-muted">Your active cURL profile is fully configured.</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/${businessId}/events`)}
|
onClick={() => navigate(eventsPath)}
|
||||||
className="px-6 py-3 bg-primary-blue hover:bg-primary-dark text-white rounded-lg shadow font-semibold text-sm transition w-full"
|
className="px-6 py-3 bg-primary-blue hover:bg-primary-dark text-white rounded-lg shadow font-semibold text-sm transition w-full"
|
||||||
>
|
>
|
||||||
Continue to Events →
|
Continue to Events →
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user