sms-extension/client/src/pages/GlobalSms.jsx

439 lines
20 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-3 rounded-md bg-delayed-bg border border-delayed-border 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">&times;</button>
</div>
)}
{success && (
<div className="px-4 py-3 rounded-md bg-badge-bg border border-badge-border text-badge-text text-sm font-medium flex justify-between items-center">
{success}
<button onClick={() => setSuccess('')} className="text-badge-text hover:opacity-75 font-bold">&times;</button>
</div>
)}
{/* Active Profile Setup Review Block */}
{activeProfile && (
<div className={`p-6 rounded-lg border ${isSetupComplete ? 'border-primary-blue bg-refresh-hover' : 'border-tags-border bg-tags-bg'} 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-border-main shadow-sm">
<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 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(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"
>
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-refresh-hover' : 'border-border-main bg-surface-white'} shadow-sm 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-badge-bg text-badge-text border border-badge-border shrink-0">
Active Profile
</span>
)}
{p.isDefault && !isActive && (
<span className="px-2 py-0.5 rounded text-[11px] font-bold tracking-wider uppercase bg-tags-bg text-tags-text border border-tags-border 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 shadow-sm 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-delayed-bg 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 shadow-sm 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-6">
<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.5 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-3 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.5 rounded-lg bg-primary-blue hover:bg-primary-dark text-white font-semibold text-sm shadow-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.5 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>
);
}