Check payload logging

This commit is contained in:
Ritul Jadhav 2026-03-30 09:49:27 +05:30
parent ab7944e866
commit 1e9947f88c
4 changed files with 208 additions and 25 deletions

View File

@ -36,7 +36,7 @@ function SubLayout({ children }) {
// Guard: redirect to / if no active business in session. // 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. // Also enforce cURL-first: only the cURL profile route is available until an active profile exists.
function BusinessGuard({ children, isGlobalSmsRoute }) { function BusinessGuard({ children, isGlobalSmsRoute }) {
const { activeBusinessId, loading, hasGlobalSms } = useBusiness(); const { activeBusinessId, loading, isSetupComplete } = useBusiness();
const location = useLocation(); const location = useLocation();
if (loading) { if (loading) {
@ -51,7 +51,7 @@ function BusinessGuard({ children, isGlobalSmsRoute }) {
return <Navigate to="/" state={{ from: location }} replace />; return <Navigate to="/" state={{ from: location }} replace />;
} }
if (!hasGlobalSms && !isGlobalSmsRoute) { if (!isSetupComplete && !isGlobalSmsRoute) {
return <Navigate to={`/${activeBusinessId}/global-sms`} replace />; return <Navigate to={`/${activeBusinessId}/global-sms`} replace />;
} }

View File

@ -29,9 +29,9 @@ export default function Sidebar() {
const navigate = useNavigate(); const navigate = useNavigate();
const navItems = [ const navItems = [
{ id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Global SMS cURL' }, { id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Omni-channel SMS' },
{ id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' }, { id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' },
{ id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' }, { id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' },
]; ];
function handleSwitch() { function handleSwitch() {
@ -70,10 +70,9 @@ export default function Sidebar() {
key={id} key={id}
to={to} to={to}
className={({ isActive }) => className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${ `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${isActive
isActive ? 'bg-refresh-hover text-primary-blue'
? 'bg-refresh-hover text-primary-blue' : 'text-text-muted hover:text-text-primary hover:bg-row-hover'
: 'text-text-muted hover:text-text-primary hover:bg-row-hover'
}` }`
} }
> >

View File

@ -10,8 +10,16 @@ const SESSION_KEY = 'sms_active_business';
export function BusinessProvider({ children }) { 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 [loading, setLoading] = useState(true); 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 // On mount: rehydrate from sessionStorage and refresh from API
useEffect(() => { useEffect(() => {
async function rehydrate() { async function rehydrate() {
@ -30,7 +38,7 @@ export function BusinessProvider({ children }) {
apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} })) apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} }))
]); ]);
setActiveBusinessState(bizRes.data); setActiveBusinessState(bizRes.data);
setHasGlobalSms(!!smsRes.data?.activeProfile); updateReadyState(smsRes.data?.activeProfile);
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ sessionStorage.setItem(SESSION_KEY, JSON.stringify({
businessId, businessId,
companyId: runtimeCompanyId || companyId || '', companyId: runtimeCompanyId || companyId || '',
@ -40,6 +48,7 @@ export function BusinessProvider({ children }) {
sessionStorage.removeItem(SESSION_KEY); sessionStorage.removeItem(SESSION_KEY);
setActiveBusinessState(null); setActiveBusinessState(null);
setHasGlobalSms(false); setHasGlobalSms(false);
setIsSetupComplete(false);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -55,15 +64,17 @@ export function BusinessProvider({ children }) {
})); }));
try { try {
const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`); const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`);
setHasGlobalSms(!!smsRes.data?.activeProfile); updateReadyState(smsRes.data?.activeProfile);
} catch { } catch {
setHasGlobalSms(false); setHasGlobalSms(false);
setIsSetupComplete(false);
} }
}, []); }, []);
const clearBusiness = useCallback(() => { const clearBusiness = useCallback(() => {
setActiveBusinessState(null); setActiveBusinessState(null);
setHasGlobalSms(false); setHasGlobalSms(false);
setIsSetupComplete(false);
sessionStorage.removeItem(SESSION_KEY); sessionStorage.removeItem(SESSION_KEY);
}, []); }, []);
@ -71,7 +82,7 @@ export function BusinessProvider({ children }) {
return ( return (
<BusinessContext.Provider value={{ <BusinessContext.Provider value={{
activeBusiness, activeBusinessId, setActiveBusiness, clearBusiness, loading, hasGlobalSms, setHasGlobalSms activeBusiness, activeBusinessId, setActiveBusiness, clearBusiness, loading, hasGlobalSms, setHasGlobalSms, isSetupComplete, setIsSetupComplete
}}> }}>
{children} {children}
</BusinessContext.Provider> </BusinessContext.Provider>

View File

@ -1,11 +1,12 @@
import { useState, useEffect, useCallback } from 'react'; 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 apiClient from '../api/client';
import { useBusiness } from '../context/BusinessContext'; import { useBusiness } from '../context/BusinessContext';
export default function GlobalSms() { export default function GlobalSms() {
const { businessId } = useParams(); const { businessId } = useParams();
const { setHasGlobalSms } = useBusiness(); const navigate = useNavigate();
const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [profiles, setProfiles] = useState([]); const [profiles, setProfiles] = useState([]);
const [activeProfileId, setActiveProfileId] = useState(null); const [activeProfileId, setActiveProfileId] = useState(null);
@ -14,30 +15,56 @@ export default function GlobalSms() {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [success, setSuccess] = useState(''); const [success, setSuccess] = useState('');
// Form state for Create / Edit // Form state for Create / Edit Profile
const [editingId, setEditingId] = useState(null); const [editingId, setEditingId] = useState(null);
const [formName, setFormName] = useState(''); const [formName, setFormName] = useState('');
const [formCurl, setFormCurl] = useState(''); const [formCurl, setFormCurl] = useState('');
const [formSetActive, setFormSetActive] = useState(true); 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 () => { const loadProfiles = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`); const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
setProfiles(res.data.profiles || []); const fetchedProfiles = res.data.profiles || [];
setActiveProfileId(res.data.activeProfileId); const fetchActiveId = res.data.activeProfileId;
setHasGlobalSms(!!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 { } catch {
setError('Failed to load cURL profiles'); setError('Failed to load cURL profiles');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [businessId, setHasGlobalSms]); }, [businessId, setHasGlobalSms, setIsSetupComplete]);
useEffect(() => { useEffect(() => {
loadProfiles(); loadProfiles();
}, [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() { function handleAddClick() {
setEditingId(null); setEditingId(null);
setFormName(''); setFormName('');
@ -51,7 +78,7 @@ export default function GlobalSms() {
setEditingId(profile.id); setEditingId(profile.id);
setFormName(profile.name); setFormName(profile.name);
setFormCurl(profile.rawCurl); setFormCurl(profile.rawCurl);
setFormSetActive(false); // only matters for create setFormSetActive(false);
setError(''); setError('');
setSuccess(''); 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) { if (loading) {
return ( return (
<div className="flex h-64 items-center justify-center"> <div className="flex h-64 items-center justify-center">
@ -118,12 +169,37 @@ 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 */} {/* Header & Stepper */}
<div> <div>
<h2 className="text-2xl font-bold text-text-primary mb-2">cURL Profiles</h2> <h2 className="text-2xl font-bold text-text-primary mb-2">Setup configuration</h2>
<p className="text-sm text-text-muted"> <p className="text-sm text-text-muted mb-8">
Manage the cURL commands used to generate and test SMS templates. The active profile will be used across the application. 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 && (
@ -139,8 +215,105 @@ export default function GlobalSms() {
</div> </div>
)} )}
{/* Active Profile Setup Review Block */}
{activeProfile && (
<div className={`p-6 rounded-xl border ${isSetupComplete ? 'border-primary-blue bg-[#f0f2fb]' : 'border-[#d47f45] bg-[#fdfaf5]'} shadow-sm`}>
<div className="flex items-center gap-3 mb-4">
<h3 className="font-bold text-text-primary text-lg">Active Setup: {activeProfile.name}</h3>
{isSetupComplete ? (
<span className="px-3 py-1 bg-badge-bg text-badge-text border border-badge-border rounded-full text-xs font-bold uppercase tracking-wide">Setup Complete</span>
) : (
<span className="px-3 py-1 bg-tags-bg text-tags-text border border-tags-border rounded-full text-xs font-bold uppercase tracking-wide">Missing Information</span>
)}
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<p className="text-sm font-medium text-text-primary">Parsed Provider Data:</p>
<ul className="space-y-2 text-sm">
<li className="flex justify-between items-center bg-surface-white p-2 rounded border border-border-soft">
<span className="text-text-muted">Provider:</span>
<span className="font-bold text-text-primary">{pData.providerName || <span className="text-error-text text-xs uppercase">Missing</span>}</span>
</li>
<li className="flex justify-between items-center bg-surface-white p-2 rounded border border-border-soft">
<span className="text-text-muted">Sender ID:</span>
<span className="font-bold text-text-primary">{pData.senderId || <span className="text-error-text text-xs uppercase">Missing</span>}</span>
</li>
<li className="flex justify-between items-center bg-surface-white p-2 rounded border border-border-soft">
<span className="text-text-muted">Entity ID:</span>
<span className="font-bold text-text-primary">{pData.dltEntityId || <span className="text-error-text text-xs uppercase">Missing</span>}</span>
</li>
<li className="flex justify-between items-center bg-surface-white p-2 rounded border border-border-soft">
<span className="text-text-muted">Auth Key:</span>
<span className="font-semibold text-text-primary font-mono">{pData.authKey ? '••••••••' : 'None setup'}</span>
</li>
</ul>
</div>
{!isSetupComplete && (
<div className="bg-surface-white p-4 rounded-lg border border-[#d47f45]/30">
<p className="text-sm font-semibold text-text-primary mb-3">Please fill in the missing fields:</p>
<form onSubmit={handleProviderSubmit} className="space-y-3">
{missingFields.includes('providerName') && (
<input
type="text"
placeholder="Provider Name (e.g. MSG91)"
value={providerForm.providerName}
onChange={e => 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') && (
<input
type="text"
placeholder="Sender ID (6 letters)"
maxLength={6}
value={providerForm.senderId}
onChange={e => 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') && (
<input
type="text"
placeholder="19-digit DLT PE ID"
value={providerForm.dltEntityId}
onChange={e => 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
/>
)}
<button
type="submit"
disabled={savingProvider}
className="w-full py-2 bg-[#d47f45] hover:bg-[#cf7b3f] text-white text-sm font-bold rounded shadow-sm transition disabled:opacity-50"
>
{savingProvider ? 'Saving...' : 'Save Required Details'}
</button>
</form>
</div>
)}
{isSetupComplete && (
<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>
<button
onClick={() => navigate(`/${businessId}/events`)}
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
</button>
</div>
)}
</div>
</div>
)}
{/* Profiles List */} {/* Profiles List */}
<div className="space-y-4"> <div className="space-y-4 pt-4 border-t border-border-soft">
<h3 className="font-bold text-text-primary text-lg">All Profiles</h3>
{profiles.length > 0 ? ( {profiles.length > 0 ? (
profiles.map(p => { profiles.map(p => {
const isActive = p.id === activeProfileId; const isActive = p.id === activeProfileId;