276 lines
11 KiB
JavaScript
276 lines
11 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import { useParams } 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 [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
|
|
const [editingId, setEditingId] = useState(null);
|
|
const [formName, setFormName] = useState('');
|
|
const [formCurl, setFormCurl] = useState('');
|
|
const [formSetActive, setFormSetActive] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadProfiles();
|
|
}, [businessId]);
|
|
|
|
async function loadProfiles() {
|
|
try {
|
|
setLoading(true);
|
|
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
|
|
setProfiles(res.data.profiles || []);
|
|
setActiveProfileId(res.data.activeProfileId);
|
|
if (res.data.activeProfileId) {
|
|
setHasGlobalSms(true);
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to load cURL profiles');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
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); // only matters for create
|
|
setError('');
|
|
setSuccess('');
|
|
}
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
if (!formName.trim() || !formCurl.trim()) return;
|
|
setSaving(true);
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
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.');
|
|
}
|
|
await loadProfiles();
|
|
setFormName('');
|
|
setFormCurl('');
|
|
setEditingId(null);
|
|
} 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) {
|
|
try {
|
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${id}/activate`);
|
|
await loadProfiles();
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to activate profile');
|
|
}
|
|
}
|
|
|
|
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">
|
|
{/* Header */}
|
|
<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.
|
|
</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">×</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">×</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Profiles List */}
|
|
<div className="space-y-4">
|
|
{profiles.length > 0 ? (
|
|
profiles.map(p => {
|
|
const isActive = p.id === activeProfileId;
|
|
return (
|
|
<div key={p.id} className={`p-5 rounded-xl border ${isActive ? 'border-primary-blue bg-[#f0f2fb]' : '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-dashed rounded-xl">
|
|
<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-xl shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-border-main bg-[#fafafa] 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>
|
|
);
|
|
}
|