sms-extension-1777874553/client/src/pages/GlobalSms.jsx
2026-03-26 14:19:26 +05:30

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">&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>
)}
{/* 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>
);
}