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

477 lines
21 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 hasProfiles = profiles.length > 0;
const isCreatingFirstProfile = !hasProfiles && !editingId;
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">&times;</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">&times;</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>
)}
{hasProfiles && (
<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.map(p => {
const isActive = p.id === activeProfileId;
return (
<div
key={p.id}
className={`rounded-xl border p-5 transition-colors ${
isActive ? 'border-primary-blue bg-white' : 'border-border-main bg-surface-white'
}`}
>
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 flex-1">
<div className="mb-3 flex flex-wrap items-center gap-3">
<h3 className="truncate text-base font-bold text-text-primary">{p.name}</h3>
{isActive && (
<span className="shrink-0 rounded-full border border-indigo-200 bg-indigo-50 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-primary-dark">
Active Profile
</span>
)}
{p.isDefault && !isActive && (
<span className="shrink-0 rounded-full border border-gray-200 bg-gray-50 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-gray-600">
Default
</span>
)}
</div>
<p className="mb-3 text-xs font-medium text-text-muted">
Updated: {new Date(p.updatedAt).toLocaleString()}
</p>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-950">
<div className="border-b border-gray-800 px-4 py-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">Raw cURL</p>
</div>
<pre className="max-h-56 overflow-auto px-4 py-4 text-xs leading-relaxed text-gray-100">
<code>{p.rawCurl}</code>
</pre>
</div>
</div>
<div className="flex shrink-0 flex-wrap items-center gap-2 lg:justify-end">
{!isActive && (
<button
onClick={() => handleActivate(p.id)}
className="rounded-lg bg-primary-blue px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark"
>
Use this cURL
</button>
)}
<button
onClick={() => handleEditClick(p)}
className="rounded-lg border border-border-main px-3 py-2 text-sm font-medium text-text-muted transition hover:border-primary-blue hover:bg-page-bg hover:text-primary-blue"
>
Edit
</button>
{profiles.length > 1 && (
<button
onClick={() => handleDelete(p.id)}
className="rounded-lg border border-border-main px-3 py-2 text-sm font-medium text-text-muted transition hover:border-error-text hover:bg-red-50 hover:text-error-text"
>
Delete
</button>
)}
</div>
</div>
</div>
);
})}
</div>
)}
{/* Inline Form (Create / Edit) */}
<div
className={`overflow-hidden rounded-xl border ${
isCreatingFirstProfile ? 'border-indigo-100 bg-white shadow-sm' : 'border-border-main bg-surface-white'
}`}
>
<div
className={`flex items-start justify-between gap-4 px-6 py-5 ${
isCreatingFirstProfile ? 'border-b border-indigo-100 bg-indigo-50/60' : 'border-b border-border-main bg-table-header'
}`}
>
<div>
<h3 className="font-bold text-text-primary text-md">
{editingId ? 'Edit Profile' : isCreatingFirstProfile ? 'Create Your First cURL Profile' : 'Add New Profile'}
</h3>
<p className="mt-1 text-sm text-text-muted">
Give this profile a recognizable name, then paste the full provider cURL command below.
</p>
</div>
{editingId && (
<button onClick={handleAddClick} className="text-sm font-semibold text-primary-blue hover:text-primary-dark hover:underline">
Switch to Add New
</button>
)}
</div>
<div className="p-6">
{isCreatingFirstProfile && (
<div className="mb-6 rounded-xl border border-indigo-100 bg-indigo-50/50 px-4 py-4">
<p className="text-sm font-semibold text-text-primary">Start by adding a cURL profile</p>
<p className="mt-1 text-sm text-text-muted">
This becomes the base for validating provider details and unlocking event template generation.
</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-semibold text-text-primary mb-1.5">Profile Name</label>
<p className="mb-2 text-xs font-medium text-text-muted">
Use a name you will recognize later, such as `Production SMS` or `Backup Provider`.
</p>
<input
type="text"
value={formName}
onChange={e => setFormName(e.target.value)}
placeholder="e.g. Production SMS, Staging Twilio"
className="w-full rounded-lg border border-border-main bg-white px-4 py-2 text-sm text-text-primary placeholder-placeholder-bg transition focus:outline-none focus:ring-2 focus:ring-primary-blue"
required
/>
</div>
<div>
<label className="block text-sm font-semibold text-text-primary mb-1.5">Raw cURL Command</label>
<p className="mb-2 text-xs font-medium text-text-muted">
Paste the full request exactly as supplied by your SMS provider. You can include the entire command.
</p>
<textarea
value={formCurl}
onChange={e => setFormCurl(e.target.value)}
placeholder="curl --request POST --url ..."
className="h-48 w-full resize-none rounded-lg border border-border-main bg-white px-4 py-3 font-mono text-sm leading-relaxed text-text-primary placeholder-placeholder-bg transition focus:outline-none focus:ring-2 focus:ring-primary-blue"
required
spellCheck="false"
/>
</div>
{!editingId && hasProfiles && (
<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>
);
}