mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: agent detail page, campaign edit slideout, integration config, auth persistence
Agent Detail (/agent/:id): - Individual agent performance page with KPI cards + call log table - Clickable agent names in dashboard table link to detail view - Back button to Team Dashboard Campaign Edit Slideout: - Edit button on each campaign card - Slideout with name, status, budget, dates - Saves via updateCampaign GraphQL mutation Integration Config Slideout: - Configure button on each integration card - Per-integration form fields (Ozonetel, WhatsApp, Facebook, etc.) - Copy webhook URL, OAuth placeholder buttons Auth Persistence: - User data persisted to localStorage on login - Session restored on page refresh — no more logout on F5 - Stale tokens cleaned up automatically Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
235
src/components/integrations/integration-edit-slideout.tsx
Normal file
235
src/components/integrations/integration-edit-slideout.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faGear, faCopy, faLink } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import type { FC, HTMLAttributes } from 'react';
|
||||
import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu';
|
||||
import { Input } from '@/components/base/input/input';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { notify } from '@/lib/toast';
|
||||
|
||||
const GearIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faGear} className={className} />
|
||||
);
|
||||
|
||||
type IntegrationType = 'ozonetel' | 'whatsapp' | 'facebook' | 'google' | 'instagram' | 'website' | 'email';
|
||||
|
||||
type IntegrationConfig = {
|
||||
type: IntegrationType;
|
||||
name: string;
|
||||
details: { label: string; value: string }[];
|
||||
webhookUrl?: string;
|
||||
};
|
||||
|
||||
type IntegrationEditSlideoutProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
integration: IntegrationConfig;
|
||||
};
|
||||
|
||||
// Field definitions per integration type
|
||||
type FieldDef = {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
type?: string;
|
||||
readOnly?: boolean;
|
||||
copyable?: boolean;
|
||||
};
|
||||
|
||||
const getFieldsForType = (integration: IntegrationConfig): FieldDef[] => {
|
||||
switch (integration.type) {
|
||||
case 'ozonetel':
|
||||
return [
|
||||
{ key: 'account', label: 'Account ID', placeholder: 'e.g. global_healthx' },
|
||||
{ key: 'apiKey', label: 'API Key', placeholder: 'Enter API key', type: 'password' },
|
||||
{ key: 'agentId', label: 'Agent ID', placeholder: 'e.g. global' },
|
||||
{ key: 'sipId', label: 'SIP ID / Extension', placeholder: 'e.g. 523590' },
|
||||
{ key: 'campaign', label: 'Campaign Name', placeholder: 'e.g. Inbound_918041763265' },
|
||||
];
|
||||
case 'whatsapp':
|
||||
return [
|
||||
{ key: 'apiKey', label: 'API Key', placeholder: 'Enter WhatsApp API key', type: 'password' },
|
||||
{ key: 'phoneNumberId', label: 'Phone Number ID', placeholder: 'e.g. 123456789012345' },
|
||||
];
|
||||
case 'facebook':
|
||||
case 'google':
|
||||
case 'instagram':
|
||||
return [];
|
||||
case 'website':
|
||||
return [
|
||||
{
|
||||
key: 'webhookUrl',
|
||||
label: 'Webhook URL',
|
||||
placeholder: '',
|
||||
readOnly: true,
|
||||
copyable: true,
|
||||
},
|
||||
];
|
||||
case 'email':
|
||||
return [
|
||||
{ key: 'smtpHost', label: 'SMTP Host', placeholder: 'e.g. smtp.gmail.com' },
|
||||
{ key: 'smtpPort', label: 'Port', placeholder: 'e.g. 587' },
|
||||
{ key: 'smtpUser', label: 'Username', placeholder: 'e.g. noreply@clinic.com' },
|
||||
{ key: 'smtpPassword', label: 'Password', placeholder: 'Enter SMTP password', type: 'password' },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialValues = (integration: IntegrationConfig): Record<string, string> => {
|
||||
const values: Record<string, string> = {};
|
||||
const detailMap = new Map(integration.details.map((d) => [d.label, d.value]));
|
||||
|
||||
switch (integration.type) {
|
||||
case 'ozonetel':
|
||||
values.account = detailMap.get('Account') ?? '';
|
||||
values.apiKey = '';
|
||||
values.agentId = detailMap.get('Agent ID') ?? '';
|
||||
values.sipId = detailMap.get('SIP Extension') ?? '';
|
||||
values.campaign = detailMap.get('Inbound Campaign') ?? '';
|
||||
break;
|
||||
case 'whatsapp':
|
||||
values.apiKey = '';
|
||||
values.phoneNumberId = '';
|
||||
break;
|
||||
case 'website':
|
||||
values.webhookUrl = integration.webhookUrl ?? '';
|
||||
break;
|
||||
case 'email':
|
||||
values.smtpHost = '';
|
||||
values.smtpPort = '587';
|
||||
values.smtpUser = '';
|
||||
values.smtpPassword = '';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
const isOAuthType = (type: IntegrationType): boolean =>
|
||||
type === 'facebook' || type === 'google' || type === 'instagram';
|
||||
|
||||
export const IntegrationEditSlideout = ({ isOpen, onOpenChange, integration }: IntegrationEditSlideoutProps) => {
|
||||
const fields = getFieldsForType(integration);
|
||||
const [values, setValues] = useState<Record<string, string>>(() => getInitialValues(integration));
|
||||
|
||||
const updateValue = (key: string, value: string) => {
|
||||
setValues((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleCopy = (value: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
notify.success('Copied', 'Value copied to clipboard');
|
||||
};
|
||||
|
||||
const handleSave = (close: () => void) => {
|
||||
notify.success('Configuration saved', `${integration.name} configuration has been saved.`);
|
||||
close();
|
||||
};
|
||||
|
||||
const handleOAuthConnect = () => {
|
||||
notify.info('OAuth Connect', `${integration.name} OAuth flow is not yet implemented. This is a placeholder.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideoutMenu isOpen={isOpen} onOpenChange={onOpenChange} isDismissable>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<SlideoutMenu.Header onClose={close}>
|
||||
<div className="flex items-center gap-3 pr-8">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-secondary">
|
||||
<GearIcon className="size-5 text-fg-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-primary">Configure {integration.name}</h2>
|
||||
<p className="text-sm text-tertiary">Edit integration settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</SlideoutMenu.Header>
|
||||
|
||||
<SlideoutMenu.Content>
|
||||
<div className="flex flex-col gap-4">
|
||||
{isOAuthType(integration.type) ? (
|
||||
<div className="flex flex-col items-center gap-4 py-8">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-secondary">
|
||||
<FontAwesomeIcon icon={faLink} className="size-6 text-fg-quaternary" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-md font-semibold text-primary">Connect {integration.name}</p>
|
||||
<p className="mt-1 text-sm text-tertiary">
|
||||
Authorize Helix Engage to access your {integration.name} account to import leads automatically.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="md"
|
||||
color="primary"
|
||||
iconLeading={({ className }: { className?: string }) => (
|
||||
<FontAwesomeIcon icon={faLink} className={className} />
|
||||
)}
|
||||
onClick={handleOAuthConnect}
|
||||
>
|
||||
Connect {integration.name}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
fields.map((field) => (
|
||||
<div key={field.key}>
|
||||
{field.readOnly && field.copyable ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-secondary">{field.label}</label>
|
||||
<div className="flex items-center gap-2 rounded-lg bg-secondary p-3">
|
||||
<code className="flex-1 truncate text-xs text-secondary">
|
||||
{values[field.key] || '\u2014'}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
iconLeading={({ className }: { className?: string }) => (
|
||||
<FontAwesomeIcon icon={faCopy} className={className} />
|
||||
)}
|
||||
onClick={() => handleCopy(values[field.key] ?? '')}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
label={field.label}
|
||||
placeholder={field.placeholder}
|
||||
type={field.type}
|
||||
value={values[field.key] ?? ''}
|
||||
onChange={(value) => updateValue(field.key, value)}
|
||||
isDisabled={field.readOnly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</SlideoutMenu.Content>
|
||||
|
||||
<SlideoutMenu.Footer>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button size="md" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
{!isOAuthType(integration.type) && (
|
||||
<Button
|
||||
size="md"
|
||||
color="primary"
|
||||
onClick={() => handleSave(close)}
|
||||
>
|
||||
Save Configuration
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SlideoutMenu.Footer>
|
||||
</>
|
||||
)}
|
||||
</SlideoutMenu>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user