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.
// 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 <Navigate to="/" state={{ from: location }} replace />;
}
if (!hasGlobalSms && !isGlobalSmsRoute) {
if (!isSetupComplete && !isGlobalSmsRoute) {
return <Navigate to={`/${activeBusinessId}/global-sms`} replace />;
}

View File

@ -29,7 +29,7 @@ export default function Sidebar() {
const navigate = useNavigate();
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: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' },
];
@ -70,8 +70,7 @@ 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
`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'
}`

View File

@ -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 (
<BusinessContext.Provider value={{
activeBusiness, activeBusinessId, setActiveBusiness, clearBusiness, loading, hasGlobalSms, setHasGlobalSms
activeBusiness, activeBusinessId, setActiveBusiness, clearBusiness, loading, hasGlobalSms, setHasGlobalSms, isSetupComplete, setIsSetupComplete
}}>
{children}
</BusinessContext.Provider>

View File

@ -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 (
<div className="flex h-64 items-center justify-center">
@ -118,12 +169,37 @@ export default function GlobalSms() {
return (
<div className="max-w-4xl mx-auto space-y-8 pb-12">
{/* Header */}
{/* Header & Stepper */}
<div>
<h2 className="text-2xl font-bold text-text-primary mb-2">cURL Profiles</h2>
<p className="text-sm text-text-muted">
Manage the cURL commands used to generate and test SMS templates. The active profile will be used across the application.
<h2 className="text-2xl font-bold text-text-primary mb-2">Setup configuration</h2>
<p className="text-sm text-text-muted mb-8">
Complete this flow to configure your cURL profile and brand provider data. You must finish setup before generating templates.
</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>
{error && (
@ -139,8 +215,105 @@ export default function GlobalSms() {
</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 */}
<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.map(p => {
const isActive = p.id === activeProfileId;