- {activeBusiness.brandName?.[0]?.toUpperCase() || 'B'}
-
-
{activeBusiness.brandName}
-
{activeBusiness.domain}
- {activeBusinessId && (
- <>
-
+
+
+ {businessImage ? (
+

+ ) : (
+
+ {activeBusiness.brandName?.[0]?.toUpperCase() || 'B'}
+
+ )}
+
+
+
{activeBusiness.brandName}
+ {activeBusinessId && (
+ <>
+
+
- {reviewError && (
-
{reviewError}
- )}
- >
- )}
-
+
+ {reviewError && (
+
{reviewError}
+ )}
+ >
+ )}
)}
diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx
index 9749a6f..40ee9f2 100644
--- a/client/src/pages/GlobalSms.jsx
+++ b/client/src/pages/GlobalSms.jsx
@@ -1,11 +1,12 @@
import { useState, useEffect, useCallback } from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
+import { useParams, useNavigate, useSearchParams } 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 [searchParams, setSearchParams] = useSearchParams();
const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
const [loading, setLoading] = useState(true);
const [profiles, setProfiles] = useState([]);
@@ -64,6 +65,28 @@ export default function GlobalSms() {
loadProfiles();
}, [loadProfiles]);
+ useEffect(() => {
+ const editProfileId = searchParams.get('editProfile');
+ if (!editProfileId || profiles.length === 0) return;
+
+ const nextParams = new URLSearchParams(searchParams);
+ nextParams.delete('editProfile');
+
+ const matchingProfile = profiles.find((profile) => profile.id === editProfileId);
+ if (matchingProfile) {
+ setEditingId(matchingProfile.id);
+ setFormName(matchingProfile.name);
+ setFormCurl(matchingProfile.rawCurl);
+ setFormSetActive(false);
+ setError('');
+ setSuccess('');
+ } else {
+ setError('The requested profile could not be found.');
+ }
+
+ setSearchParams(nextParams, { replace: true });
+ }, [profiles, searchParams, setSearchParams]);
+
const activeProfile = profiles.find(p => p.id === activeProfileId) || null;
const hasProfiles = profiles.length > 0;
const isCreatingFirstProfile = !hasProfiles && !editingId;
diff --git a/client/src/pages/Providers.jsx b/client/src/pages/Providers.jsx
index dce5f87..0bb1072 100644
--- a/client/src/pages/Providers.jsx
+++ b/client/src/pages/Providers.jsx
@@ -1,12 +1,76 @@
-import { useState, useEffect } from 'react';
-import { useParams } from 'react-router-dom';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
import apiClient from '../api/client';
+import { useBusiness } from '../context/BusinessContext';
+
+const DESKTOP_SPLIT_QUERY = '(min-width: 1100px)';
+const DEFAULT_LIST_PANE_WIDTH = 340;
+const MIN_LIST_PANE_WIDTH = 280;
+const MAX_LIST_PANE_WIDTH = 420;
+const MIN_DETAIL_PANE_WIDTH = 440;
+
+function clamp(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+}
+
+function getMissingProviderFields(profile) {
+ const provider = profile?.provider || {};
+ const missing = [];
+
+ if (!provider.providerName) missing.push('Provider Name');
+ if (!provider.senderId) missing.push('Sender ID');
+ if (!provider.dltEntityId) missing.push('DLT Entity ID');
+
+ return missing;
+}
+
+function isProviderSetupComplete(profile) {
+ return getMissingProviderFields(profile).length === 0;
+}
+
+function formatUpdatedAt(value) {
+ if (!value) return 'Not updated yet';
+
+ try {
+ return new Date(value).toLocaleString();
+ } catch {
+ return 'Not updated yet';
+ }
+}
+
+function buildProviderSummary(profile) {
+ const provider = profile?.provider || {};
+ const parts = [];
+
+ if (provider.providerName) parts.push(provider.providerName);
+ if (provider.senderId) parts.push(`Sender ${provider.senderId}`);
+ if (provider.dltEntityId) parts.push('DLT added');
+
+ return parts.length > 0 ? parts.join(' • ') : 'Provider details not completed yet';
+}
+
+function ProfileStatusPill({ complete }) {
+ return (
+
+ {complete ? 'Complete' : 'Missing Fields'}
+
+ );
+}
export default function Providers() {
const { businessId } = useParams();
+ const navigate = useNavigate();
+ const { refreshOnboardingState } = useBusiness();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
- const [activeProfile, setActiveProfile] = useState(null);
+ const [profiles, setProfiles] = useState([]);
+ const [activeProfileId, setActiveProfileId] = useState('');
+ const [selectedProfileId, setSelectedProfileId] = useState('');
const [form, setForm] = useState({
providerName: '',
senderId: '',
@@ -15,54 +79,192 @@ export default function Providers() {
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
+ const [copiedProfileId, setCopiedProfileId] = useState('');
+ const [isDesktopSplit, setIsDesktopSplit] = useState(false);
+ const [listPaneWidth, setListPaneWidth] = useState(DEFAULT_LIST_PANE_WIDTH);
+ const layoutRef = useRef(null);
+ const copyTimeoutRef = useRef(null);
- useEffect(() => {
- async function load() {
- try {
- const [activeRes, providerRes] = await Promise.all([
- apiClient.get(`/api/businesses/${businessId}/global-sms/active`),
- apiClient.get(`/api/businesses/${businessId}/providers`),
- ]);
+ const globalSmsPath = `/${businessId}/global-sms`;
- setActiveProfile(activeRes.data?.activeProfile || null);
- setForm({
- providerName: providerRes.data?.providerName || '',
- senderId: providerRes.data?.senderId || '',
- dltEntityId: providerRes.data?.dltEntityId || '',
- authKey: providerRes.data?.authKey || '',
- });
- } catch (err) {
- setError(err.response?.data?.error || 'Failed to load provider configuration');
- } finally {
- setLoading(false);
- }
+ const loadProfiles = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
+ const fetchedProfiles = res.data?.profiles || [];
+ const nextActiveProfileId = String(res.data?.activeProfileId || '');
+
+ setProfiles(fetchedProfiles);
+ setActiveProfileId(nextActiveProfileId);
+ setSelectedProfileId((currentSelectedProfileId) => (
+ fetchedProfiles.some((profile) => profile.id === currentSelectedProfileId)
+ ? currentSelectedProfileId
+ : ''
+ ));
+ } catch (err) {
+ setError(err.response?.data?.error || 'Failed to load provider profiles');
+ } finally {
+ setLoading(false);
}
- load();
}, [businessId]);
+ useEffect(() => {
+ loadProfiles();
+ }, [loadProfiles]);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia(DESKTOP_SPLIT_QUERY);
+ const syncLayoutMode = (event) => setIsDesktopSplit(event.matches);
+
+ setIsDesktopSplit(mediaQuery.matches);
+
+ if (typeof mediaQuery.addEventListener === 'function') {
+ mediaQuery.addEventListener('change', syncLayoutMode);
+ return () => mediaQuery.removeEventListener('change', syncLayoutMode);
+ }
+
+ mediaQuery.addListener(syncLayoutMode);
+ return () => mediaQuery.removeListener(syncLayoutMode);
+ }, []);
+
+ useEffect(() => () => {
+ if (copyTimeoutRef.current) {
+ clearTimeout(copyTimeoutRef.current);
+ }
+ }, []);
+
+ const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) || null;
+
+ useEffect(() => {
+ if (!selectedProfile) {
+ setForm({
+ providerName: '',
+ senderId: '',
+ dltEntityId: '',
+ authKey: '',
+ });
+ return;
+ }
+
+ const provider = selectedProfile.provider || {};
+ setForm({
+ providerName: provider.providerName || '',
+ senderId: provider.senderId || '',
+ dltEntityId: provider.dltEntityId || '',
+ authKey: provider.authKey || '',
+ });
+ }, [selectedProfile]);
+
function handleChange(field, value) {
- setForm(prev => ({ ...prev, [field]: value }));
+ setForm((prev) => ({ ...prev, [field]: value }));
}
- async function handleSave(e) {
- e.preventDefault();
+ function handleSelectProfile(profileId) {
+ setSelectedProfileId(profileId);
+ setError('');
+ setSuccess('');
+ }
+
+ function handleResizeStart(event) {
+ if (!isDesktopSplit) return;
+
+ event.preventDefault();
+ const containerBounds = layoutRef.current?.getBoundingClientRect();
+ if (!containerBounds) return;
+
+ const maxAllowedWidth = clamp(
+ containerBounds.width - MIN_DETAIL_PANE_WIDTH,
+ MIN_LIST_PANE_WIDTH,
+ MAX_LIST_PANE_WIDTH,
+ );
+
+ function handlePointerMove(moveEvent) {
+ const nextWidth = clamp(
+ moveEvent.clientX - containerBounds.left,
+ MIN_LIST_PANE_WIDTH,
+ maxAllowedWidth,
+ );
+ setListPaneWidth(nextWidth);
+ }
+
+ function handlePointerUp() {
+ document.body.style.userSelect = '';
+ window.removeEventListener('mousemove', handlePointerMove);
+ window.removeEventListener('mouseup', handlePointerUp);
+ }
+
+ document.body.style.userSelect = 'none';
+ window.addEventListener('mousemove', handlePointerMove);
+ window.addEventListener('mouseup', handlePointerUp);
+ }
+
+ async function handleActivate(profile) {
+ if (!profile?.id) return;
+
+ try {
+ setError('');
+ setSuccess('');
+ await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${profile.id}/activate`);
+ setSelectedProfileId(profile.id);
+ await loadProfiles();
+ await refreshOnboardingState(businessId).catch(() => null);
+ setSuccess(`${profile.name} is now the active profile.`);
+ } catch (err) {
+ setError(err.response?.data?.error || 'Failed to activate profile');
+ }
+ }
+
+ async function handleCopyCurl(profile) {
+ if (!profile?.rawCurl) return;
+
+ try {
+ if (!navigator?.clipboard?.writeText) {
+ throw new Error('Clipboard API unavailable');
+ }
+
+ await navigator.clipboard.writeText(profile.rawCurl);
+ setCopiedProfileId(profile.id);
+
+ if (copyTimeoutRef.current) {
+ clearTimeout(copyTimeoutRef.current);
+ }
+
+ copyTimeoutRef.current = window.setTimeout(() => {
+ setCopiedProfileId('');
+ }, 1800);
+ } catch {
+ setError('Failed to copy the cURL command.');
+ }
+ }
+
+ async function handleSave(event) {
+ event.preventDefault();
+
+ if (!selectedProfile?.id) return;
+
setSaving(true);
setError('');
setSuccess('');
+
if (form.senderId && !/^[A-Za-z]{6}$/.test(form.senderId)) {
setError('DLT Sender ID must be exactly 6 alphabet characters');
setSaving(false);
return;
}
+
try {
- const res = await apiClient.post(`/api/businesses/${businessId}/providers`, form);
- setForm({
- providerName: res.data?.providerName || '',
- senderId: res.data?.senderId || '',
- dltEntityId: res.data?.dltEntityId || '',
- authKey: res.data?.authKey || '',
+ await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${selectedProfile.id}`, {
+ provider: {
+ providerName: form.providerName,
+ senderId: form.senderId.toUpperCase(),
+ dltEntityId: form.dltEntityId,
+ authKey: form.authKey,
+ },
});
- setSuccess('Provider configuration saved successfully.');
+
+ await loadProfiles();
+ await refreshOnboardingState(businessId).catch(() => null);
+ setSuccess(`Provider configuration saved for ${selectedProfile.name}.`);
} catch (err) {
setError(err.response?.data?.error || 'Failed to save configuration');
} finally {
@@ -79,98 +281,309 @@ export default function Providers() {
}
return (
-
-
-
Provider Configuration
-
Edit the provider details stored on the active cURL profile.
- {activeProfile && (
-
- Active Profile: {activeProfile.name}
+
+
+
+
Provider Configuration
+
+ Pick a saved profile to review its complete request and manage the provider details stored against it.
- )}
+
+
{error && (
-
+
{error}
)}
{success && (
-
+
{success}
)}
-