439 lines
19 KiB
JavaScript
439 lines
19 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
|
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 navigate = useNavigate();
|
|
const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
|
|
const [loading, setLoading] = useState(true);
|
|
const [profiles, setProfiles] = useState([]);
|
|
const [activeProfileId, setActiveProfileId] = useState(null);
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
|
|
// 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 eventsPath = `/${businessId}/events`;
|
|
|
|
const loadProfiles = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
|
|
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 || '',
|
|
});
|
|
|
|
return { activeProfile, hasProfile, complete };
|
|
} catch {
|
|
setError('Failed to load cURL profiles');
|
|
setHasGlobalSms(false);
|
|
setIsSetupComplete(false);
|
|
return { activeProfile: null, hasProfile: false, complete: false };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [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('');
|
|
setFormCurl('');
|
|
setFormSetActive(profiles.length === 0);
|
|
setError('');
|
|
setSuccess('');
|
|
}
|
|
|
|
function handleEditClick(profile) {
|
|
setEditingId(profile.id);
|
|
setFormName(profile.name);
|
|
setFormCurl(profile.rawCurl);
|
|
setFormSetActive(false);
|
|
setError('');
|
|
setSuccess('');
|
|
}
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
if (!formName.trim() || !formCurl.trim()) return;
|
|
setSaving(true);
|
|
setError('');
|
|
setSuccess('');
|
|
const shouldAutoAdvance = !isSetupComplete;
|
|
|
|
try {
|
|
if (editingId) {
|
|
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${editingId}`, {
|
|
name: formName,
|
|
rawCurl: formCurl,
|
|
});
|
|
setSuccess('Profile updated successfully.');
|
|
} else {
|
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles`, {
|
|
name: formName,
|
|
rawCurl: formCurl,
|
|
setActive: formSetActive,
|
|
});
|
|
setSuccess('Profile created successfully.');
|
|
}
|
|
const nextState = await loadProfiles();
|
|
setFormName('');
|
|
setFormCurl('');
|
|
setEditingId(null);
|
|
if (shouldAutoAdvance && nextState.complete) {
|
|
navigate(eventsPath);
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to save cURL profile');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleDelete(id) {
|
|
if (!window.confirm('Delete this cURL profile?')) return;
|
|
try {
|
|
await apiClient.delete(`/api/businesses/${businessId}/global-sms/profiles/${id}`);
|
|
await loadProfiles();
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to delete profile');
|
|
}
|
|
}
|
|
|
|
async function handleActivate(id) {
|
|
const shouldAutoAdvance = !isSetupComplete;
|
|
try {
|
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${id}/activate`);
|
|
const nextState = await loadProfiles();
|
|
if (shouldAutoAdvance && nextState.complete) {
|
|
navigate(eventsPath);
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to activate profile');
|
|
}
|
|
}
|
|
|
|
async function handleProviderSubmit(e) {
|
|
e.preventDefault();
|
|
if (!activeProfileId) return;
|
|
setSavingProvider(true);
|
|
setError('');
|
|
setSuccess('');
|
|
const shouldAutoAdvance = !isSetupComplete;
|
|
|
|
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!');
|
|
const nextState = await loadProfiles();
|
|
if (shouldAutoAdvance && nextState.complete) {
|
|
navigate(eventsPath);
|
|
}
|
|
} 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">
|
|
<span className="w-8 h-8 border-4 border-spinner-track border-t-primary-blue rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto space-y-8 pb-12">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-text-primary mb-2">Omni-channel SMS</h2>
|
|
<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.
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="px-4 py-2 rounded-md bg-white border border-gray-200 text-error-text text-sm font-medium flex justify-between items-center">
|
|
{error}
|
|
<button onClick={() => setError('')} className="text-error-text hover:text-red-900 font-bold">×</button>
|
|
</div>
|
|
)}
|
|
{success && (
|
|
<div className="px-4 py-2 rounded-md bg-white border border-gray-200 text-gray-700 text-sm font-medium flex justify-between items-center">
|
|
{success}
|
|
<button onClick={() => setSuccess('')} className="text-gray-700 hover:opacity-75 font-bold">×</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Active Profile Setup Review Block */}
|
|
{activeProfile && (
|
|
<div className={`p-5 rounded-lg border ${isSetupComplete ? 'border-primary-blue bg-white' : 'border-gray-200 bg-white'} `}>
|
|
<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-white text-gray-700 border border-gray-200 rounded-full text-xs font-bold uppercase tracking-wide">Setup Complete</span>
|
|
) : (
|
|
<span className="px-3 py-1 bg-white text-gray-700 border border-gray-200 rounded-full text-xs font-bold uppercase tracking-wide">Missing Information</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-5">
|
|
<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-border-main ">
|
|
<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-primary-blue hover:bg-primary-dark text-white text-sm font-bold rounded 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(eventsPath)}
|
|
className="px-6 py-2 bg-primary-blue hover:bg-primary-dark text-white rounded-lg font-semibold text-sm transition w-full"
|
|
>
|
|
Continue to Events →
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Profiles List */}
|
|
<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;
|
|
return (
|
|
<div key={p.id} className={`p-5 rounded-lg border ${isActive ? 'border-primary-blue bg-white' : 'border-border-main bg-surface-white'} flex flex-col md:flex-row gap-4 items-start md:items-center justify-between transition-colors`}>
|
|
<div className="flex-1 overflow-hidden">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<h3 className="font-bold text-text-primary text-base truncate">{p.name}</h3>
|
|
{isActive && (
|
|
<span className="px-2 py-0.5 rounded text-[11px] font-bold tracking-wider uppercase bg-white text-gray-700 border border-gray-200 shrink-0">
|
|
Active Profile
|
|
</span>
|
|
)}
|
|
{p.isDefault && !isActive && (
|
|
<span className="px-2 py-0.5 rounded text-[11px] font-bold tracking-wider uppercase bg-white text-gray-700 border border-gray-200 shrink-0">
|
|
Default
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-text-muted font-medium mb-2">Updated: {new Date(p.updatedAt).toLocaleString()}</p>
|
|
<p className="text-sm font-mono text-text-muted bg-page-bg p-2 rounded border border-border-soft overflow-hidden whitespace-nowrap text-ellipsis max-w-xl">
|
|
{p.rawCurl}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{!isActive && (
|
|
<button
|
|
onClick={() => handleActivate(p.id)}
|
|
className="px-4 py-2 bg-primary-blue hover:bg-primary-dark text-white text-sm font-semibold rounded-lg transition"
|
|
>
|
|
Use this cURL
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => handleEditClick(p)}
|
|
className="px-3 py-2 border border-border-main text-text-muted hover:text-primary-blue hover:border-primary-blue rounded-lg text-sm font-medium transition"
|
|
>
|
|
Edit
|
|
</button>
|
|
{profiles.length > 1 && (
|
|
<button
|
|
onClick={() => handleDelete(p.id)}
|
|
className="px-3 py-2 border border-border-main text-text-muted hover:text-error-text hover:border-error-text hover:bg-white rounded-lg text-sm font-medium transition"
|
|
>
|
|
Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="text-center py-12 bg-surface-white border border-border-main border-dashed rounded-lg">
|
|
<p className="text-sm font-medium text-text-muted mb-4">No cURL profiles configured yet.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Inline Form (Create / Edit) */}
|
|
<div className="bg-surface-white border border-border-main rounded-lg overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-border-main bg-table-header flex items-center justify-between">
|
|
<h3 className="font-bold text-text-primary text-md">
|
|
{editingId ? 'Edit Profile' : 'Add New Profile'}
|
|
</h3>
|
|
{editingId && (
|
|
<button onClick={handleAddClick} className="text-sm font-semibold text-primary-blue hover:underline">
|
|
Switch to Add New
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="p-5">
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-text-primary mb-1.5">Profile Name</label>
|
|
<input
|
|
type="text"
|
|
value={formName}
|
|
onChange={e => setFormName(e.target.value)}
|
|
placeholder="e.g. Production SMS, Staging Twilio"
|
|
className="w-full px-4 py-2 rounded-lg bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue transition text-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-text-primary mb-1.5">Raw cURL Command</label>
|
|
<textarea
|
|
value={formCurl}
|
|
onChange={e => setFormCurl(e.target.value)}
|
|
placeholder="curl --request POST --url ..."
|
|
className="w-full h-40 px-4 py-2 rounded-lg font-mono text-sm bg-page-bg border border-border-main text-text-primary placeholder-placeholder-bg focus:outline-none focus:ring-2 focus:ring-primary-blue transition resize-none leading-relaxed"
|
|
required
|
|
spellCheck="false"
|
|
/>
|
|
</div>
|
|
{!editingId && profiles.length > 0 && (
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
className="w-4 h-4 text-primary-blue rounded border-border-main focus:ring-primary-blue"
|
|
checked={formSetActive}
|
|
onChange={e => setFormSetActive(e.target.checked)}
|
|
/>
|
|
<span className="text-sm font-semibold text-text-primary">Set as active profile immediately</span>
|
|
</label>
|
|
)}
|
|
<div className="flex gap-3 pt-2">
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="px-6 py-2 rounded-lg bg-primary-blue hover:bg-primary-dark text-white font-semibold text-sm transition disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{saving ? <><span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> Saving…</> : (editingId ? 'Update Profile' : 'Save Profile')}
|
|
</button>
|
|
{editingId && (
|
|
<button
|
|
type="button"
|
|
onClick={handleAddClick}
|
|
disabled={saving}
|
|
className="px-5 py-2 rounded-lg border border-border-main text-text-primary hover:bg-page-bg font-medium text-sm transition"
|
|
>
|
|
Cancel Edit
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|