Files
helix-engage/src/components/forms/telephony-form.tsx
saridsa2 d730cda06d feat(config): add Ozonetel admin credential fields to telephony form
Admin username + password inputs in the Ozonetel section for supervisor
barge/whisper/listen access. Follows existing masked password pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:58:29 +05:30

203 lines
8.1 KiB
TypeScript

import { Input } from '@/components/base/input/input';
// Telephony form — covers Ozonetel cloud-call-center, the Ozonetel WebRTC
// gateway, and Exotel REST API credentials. Mirrors the TelephonyConfig shape
// in helix-engage-server/src/config/telephony.defaults.ts.
//
// Secrets (ozonetel.agentPassword, exotel.apiToken) come back from the GET
// endpoint as the sentinel '***masked***' — the form preserves that sentinel
// untouched unless the admin actually edits the field, in which case the
// backend overwrites the stored value. This is the same convention used by
// TelephonyConfigService.getMaskedConfig / updateConfig.
export type TelephonyFormValues = {
ozonetel: {
agentId: string;
agentPassword: string;
did: string;
sipId: string;
campaignName: string;
adminUsername: string;
adminPassword: string;
};
sip: {
domain: string;
wsPort: string;
};
exotel: {
apiKey: string;
apiToken: string;
accountSid: string;
subdomain: string;
};
};
export const emptyTelephonyFormValues = (): TelephonyFormValues => ({
ozonetel: {
agentId: '',
agentPassword: '',
did: '',
sipId: '',
campaignName: '',
adminUsername: '',
adminPassword: '',
},
sip: {
domain: 'blr-pub-rtc4.ozonetel.com',
wsPort: '444',
},
exotel: {
apiKey: '',
apiToken: '',
accountSid: '',
subdomain: 'api.exotel.com',
},
});
type TelephonyFormProps = {
value: TelephonyFormValues;
onChange: (value: TelephonyFormValues) => void;
};
export const TelephonyForm = ({ value, onChange }: TelephonyFormProps) => {
const patchOzonetel = (updates: Partial<TelephonyFormValues['ozonetel']>) =>
onChange({ ...value, ozonetel: { ...value.ozonetel, ...updates } });
const patchSip = (updates: Partial<TelephonyFormValues['sip']>) =>
onChange({ ...value, sip: { ...value.sip, ...updates } });
const patchExotel = (updates: Partial<TelephonyFormValues['exotel']>) =>
onChange({ ...value, exotel: { ...value.exotel, ...updates } });
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-primary">Ozonetel Cloud Agent</h3>
<p className="mt-1 text-xs text-tertiary">
Outbound dialing, SIP registration, and agent provisioning. Get these values from your
Ozonetel dashboard under Admin Users and Numbers.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="Agent ID"
placeholder="e.g. agent001"
value={value.ozonetel.agentId}
onChange={(v) => patchOzonetel({ agentId: v })}
/>
<Input
label="Agent password"
type="password"
placeholder="Leave '***masked***' to keep current"
value={value.ozonetel.agentPassword}
onChange={(v) => patchOzonetel({ agentPassword: v })}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="Default DID"
placeholder="Primary hospital number"
value={value.ozonetel.did}
onChange={(v) => patchOzonetel({ did: v })}
/>
<Input
label="SIP ID"
placeholder="Softphone extension"
value={value.ozonetel.sipId}
onChange={(v) => patchOzonetel({ sipId: v })}
/>
</div>
<Input
label="Campaign name"
placeholder="CloudAgent campaign for outbound dial"
value={value.ozonetel.campaignName}
onChange={(v) => patchOzonetel({ campaignName: v })}
/>
<div>
<h4 className="mt-2 text-xs font-semibold text-secondary">Supervisor Access</h4>
<p className="mt-0.5 text-xs text-tertiary">
Ozonetel portal admin credentials required for supervisor barge/whisper/listen.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="Admin username"
placeholder="Ozonetel portal admin login"
value={value.ozonetel.adminUsername}
onChange={(v) => patchOzonetel({ adminUsername: v })}
/>
<Input
label="Admin password"
type="password"
placeholder="Leave '***masked***' to keep current"
value={value.ozonetel.adminPassword}
onChange={(v) => patchOzonetel({ adminPassword: v })}
/>
</div>
</section>
<section className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-primary">SIP Gateway (WebRTC)</h3>
<p className="mt-1 text-xs text-tertiary">
Used by the staff portal softphone. Defaults work for most Indian Ozonetel tenants only
change if Ozonetel support instructs you to.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="SIP domain"
placeholder="blr-pub-rtc4.ozonetel.com"
value={value.sip.domain}
onChange={(v) => patchSip({ domain: v })}
/>
<Input
label="WebSocket port"
placeholder="444"
value={value.sip.wsPort}
onChange={(v) => patchSip({ wsPort: v })}
/>
</div>
</section>
<section className="flex flex-col gap-4">
<div>
<h3 className="text-sm font-semibold text-primary">Exotel (SMS + inbound numbers)</h3>
<p className="mt-1 text-xs text-tertiary">
Optional only required if you use Exotel for SMS or want inbound number management from
this portal.
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="API key"
placeholder="Exotel API key"
value={value.exotel.apiKey}
onChange={(v) => patchExotel({ apiKey: v })}
/>
<Input
label="API token"
type="password"
placeholder="Leave '***masked***' to keep current"
value={value.exotel.apiToken}
onChange={(v) => patchExotel({ apiToken: v })}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="Account SID"
placeholder="Exotel account SID"
value={value.exotel.accountSid}
onChange={(v) => patchExotel({ accountSid: v })}
/>
<Input
label="Subdomain"
placeholder="api.exotel.com"
value={value.exotel.subdomain}
onChange={(v) => patchExotel({ subdomain: v })}
/>
</div>
</section>
</div>
);
};