240 lines
9.1 KiB
JavaScript
240 lines
9.1 KiB
JavaScript
import { useEffect, useMemo, useState } from 'react';
|
|
import apiClient from '../api/client';
|
|
|
|
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
|
|
|
|
function buildProfilePatchPayload(inputs = [], values = {}) {
|
|
const provider = {};
|
|
const profileInputValues = {};
|
|
|
|
inputs.forEach((input) => {
|
|
const rawValue = String(values[input.key] ?? '').trim();
|
|
if (!rawValue) return;
|
|
|
|
if (BASE_PROFILE_KEYS.has(input.key)) {
|
|
provider[input.key] = input.key === 'senderId' ? rawValue.toUpperCase() : rawValue;
|
|
return;
|
|
}
|
|
|
|
profileInputValues[input.key] = rawValue;
|
|
});
|
|
|
|
return {
|
|
...(Object.keys(provider).length > 0 ? { provider } : {}),
|
|
...(Object.keys(profileInputValues).length > 0 ? { profileInputValues } : {}),
|
|
};
|
|
}
|
|
|
|
function getInitialValues(inputs = []) {
|
|
return inputs.reduce((accumulator, input) => {
|
|
accumulator[input.key] = input.value || '';
|
|
return accumulator;
|
|
}, {});
|
|
}
|
|
|
|
export default function WhitelistModal({ businessId, template, boundProfile, onClose, onSuccess }) {
|
|
const [profile, setProfile] = useState(boundProfile);
|
|
const [profileForm, setProfileForm] = useState({});
|
|
const [templateId, setTemplateId] = useState('');
|
|
const [toNumber, setToNumber] = useState('');
|
|
const [savingProfile, setSavingProfile] = useState(false);
|
|
const [publishing, setPublishing] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [step, setStep] = useState('profile');
|
|
|
|
const missingInputs = useMemo(
|
|
() => profile?.executionReadiness?.missingProfileInputs || [],
|
|
[profile],
|
|
);
|
|
|
|
useEffect(() => {
|
|
setProfile(boundProfile);
|
|
}, [boundProfile]);
|
|
|
|
useEffect(() => {
|
|
setProfileForm(getInitialValues(missingInputs));
|
|
}, [missingInputs]);
|
|
|
|
useEffect(() => {
|
|
if (!boundProfile) {
|
|
setError('The cURL profile bound to this template is missing. Re-select the template from Events before publishing.');
|
|
setStep('profile');
|
|
return;
|
|
}
|
|
|
|
setError('');
|
|
setStep(missingInputs.length > 0 ? 'profile' : 'publish');
|
|
}, [boundProfile, missingInputs]);
|
|
|
|
async function handleProfileSubmit(event) {
|
|
event.preventDefault();
|
|
if (!profile?.id || missingInputs.length === 0) return;
|
|
|
|
setSavingProfile(true);
|
|
setError('');
|
|
|
|
try {
|
|
const payload = buildProfilePatchPayload(missingInputs, profileForm);
|
|
const res = await apiClient.patch(
|
|
`/api/businesses/${businessId}/global-sms/profiles/${profile.id}`,
|
|
payload,
|
|
);
|
|
|
|
setProfile(res.data);
|
|
setStep(res.data?.executionReadiness?.missingProfileInputs?.length > 0 ? 'profile' : 'publish');
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to save required profile fields');
|
|
} finally {
|
|
setSavingProfile(false);
|
|
}
|
|
}
|
|
|
|
async function handlePublish(event) {
|
|
event.preventDefault();
|
|
if (!templateId.trim() || !toNumber.trim()) return;
|
|
|
|
setPublishing(true);
|
|
setError('');
|
|
|
|
try {
|
|
await apiClient.post(`/api/businesses/${businessId}/templates/${template.eventSlug}/publish`, {
|
|
templateId: templateId.trim(),
|
|
toNumber: toNumber.trim(),
|
|
});
|
|
await Promise.resolve(onSuccess());
|
|
} catch (err) {
|
|
if (err.response?.data?.missingFields?.length) {
|
|
setError(`Missing profile fields: ${err.response.data.missingFields.join(', ')}`);
|
|
setStep('profile');
|
|
} else {
|
|
setError(err.response?.data?.error || 'Failed to publish template');
|
|
}
|
|
} finally {
|
|
setPublishing(false);
|
|
}
|
|
}
|
|
|
|
const isProfileMissing = !profile?.id;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto bg-gray-900/50 pb-10 pt-10 backdrop-blur-sm">
|
|
<div className="my-auto w-full max-w-md rounded-lg border border-border-main bg-surface-white p-5">
|
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full border border-gray-200 bg-white">
|
|
<span className="text-xl">✅</span>
|
|
</div>
|
|
|
|
<h3 className="mb-1 text-center text-lg font-bold text-text-primary">
|
|
{step === 'profile' ? 'Complete Profile Setup' : 'Publish Template'}
|
|
</h3>
|
|
<p className="mb-1 text-center text-sm text-text-muted">
|
|
{step === 'profile'
|
|
? 'Complete the required fields on the bound cURL profile before publishing.'
|
|
: 'Provide the DLT template ID and destination number to complete publish.'}
|
|
</p>
|
|
<p className="mb-2 text-center text-sm font-semibold capitalize text-text-primary">
|
|
{template.eventLabel || template.eventSlug.replace(/_/g, ' ')}
|
|
</p>
|
|
{profile && (
|
|
<p className="mb-6 text-center text-xs font-semibold uppercase tracking-wide text-text-muted">
|
|
Bound Profile: {profile.name}
|
|
</p>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mb-4 rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-error-text">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{step === 'profile' ? (
|
|
<form onSubmit={handleProfileSubmit} className="space-y-4">
|
|
{missingInputs.map((input) => (
|
|
<div key={input.key}>
|
|
<label className="mb-1.5 block text-sm font-semibold text-text-primary">{input.label}</label>
|
|
<input
|
|
type={input.secret ? 'password' : 'text'}
|
|
value={profileForm[input.key] || ''}
|
|
onChange={(event) => setProfileForm((current) => ({
|
|
...current,
|
|
[input.key]: input.key === 'senderId'
|
|
? event.target.value.toUpperCase()
|
|
: event.target.value,
|
|
}))}
|
|
className="w-full rounded-lg border border-border-main bg-page-bg px-4 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
placeholder={input.label}
|
|
required={input.required !== false}
|
|
autoFocus={input.key === missingInputs[0]?.key}
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={savingProfile}
|
|
className="flex-1 rounded-lg border border-border-main py-2 text-sm font-medium text-text-primary transition hover:bg-page-bg disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={savingProfile || isProfileMissing || missingInputs.some((input) => !String(profileForm[input.key] || '').trim())}
|
|
className="flex-1 rounded-lg bg-primary-blue py-2 text-sm font-semibold text-white transition hover:bg-primary-dark disabled:opacity-50"
|
|
>
|
|
{savingProfile ? 'Saving…' : 'Save Details'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
) : (
|
|
<form onSubmit={handlePublish} className="space-y-4">
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-semibold text-text-primary">DLT Template ID</label>
|
|
<input
|
|
type="text"
|
|
value={templateId}
|
|
onChange={(event) => setTemplateId(event.target.value)}
|
|
placeholder="e.g. 1234567890987654321"
|
|
className="w-full rounded-lg border border-border-main bg-page-bg px-4 py-2 font-mono text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
autoFocus
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-semibold text-text-primary">Destination Phone Number</label>
|
|
<input
|
|
type="text"
|
|
value={toNumber}
|
|
onChange={(event) => setToNumber(event.target.value)}
|
|
placeholder="e.g. 919876543210"
|
|
className="w-full rounded-lg border border-border-main bg-page-bg px-4 py-2 font-mono text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
required
|
|
/>
|
|
<p className="mt-1 text-xs text-text-muted">This sends the publish-triggering SMS request.</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={publishing}
|
|
className="flex-1 rounded-lg border border-border-main py-2 text-sm font-medium text-text-primary transition hover:bg-page-bg disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={publishing || !templateId.trim() || !toNumber.trim()}
|
|
className="flex-1 rounded-lg bg-primary-blue py-2 text-sm font-semibold text-white transition hover:bg-primary-dark disabled:opacity-50"
|
|
>
|
|
{publishing ? 'Publishing…' : 'Publish'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|