mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +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:
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faPhone,
|
||||
@@ -10,12 +11,23 @@ import {
|
||||
faCopy,
|
||||
faCircleCheck,
|
||||
faCircleXmark,
|
||||
faGear,
|
||||
} from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { IntegrationEditSlideout } from '@/components/integrations/integration-edit-slideout';
|
||||
import { notify } from '@/lib/toast';
|
||||
|
||||
type IntegrationType = 'ozonetel' | 'whatsapp' | 'facebook' | 'google' | 'instagram' | 'website' | 'email';
|
||||
|
||||
type IntegrationConfig = {
|
||||
type: IntegrationType;
|
||||
name: string;
|
||||
details: { label: string; value: string }[];
|
||||
webhookUrl?: string;
|
||||
};
|
||||
|
||||
type IntegrationStatus = 'connected' | 'disconnected' | 'configured';
|
||||
|
||||
type IntegrationCardProps = {
|
||||
@@ -26,6 +38,7 @@ type IntegrationCardProps = {
|
||||
status: IntegrationStatus;
|
||||
details: { label: string; value: string }[];
|
||||
webhookUrl?: string;
|
||||
onConfigure?: () => void;
|
||||
};
|
||||
|
||||
const statusConfig: Record<IntegrationStatus, { color: 'success' | 'error' | 'warning'; label: string }> = {
|
||||
@@ -34,7 +47,7 @@ const statusConfig: Record<IntegrationStatus, { color: 'success' | 'error' | 'wa
|
||||
configured: { color: 'warning', label: 'Configured' },
|
||||
};
|
||||
|
||||
const IntegrationCard = ({ name, description, icon, iconColor, status, details, webhookUrl }: IntegrationCardProps) => {
|
||||
const IntegrationCard = ({ name, description, icon, iconColor, status, details, webhookUrl, onConfigure }: IntegrationCardProps) => {
|
||||
const statusCfg = statusConfig[status];
|
||||
|
||||
const copyWebhook = () => {
|
||||
@@ -56,10 +69,24 @@ const IntegrationCard = ({ name, description, icon, iconColor, status, details,
|
||||
<p className="text-xs text-tertiary">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge size="sm" color={statusCfg.color} type="pill-color">
|
||||
<FontAwesomeIcon icon={status === 'connected' ? faCircleCheck : faCircleXmark} className="mr-1 size-3" />
|
||||
{statusCfg.label}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
{onConfigure && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
iconLeading={({ className }: { className?: string }) => (
|
||||
<FontAwesomeIcon icon={faGear} className={className} />
|
||||
)}
|
||||
onClick={onConfigure}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
<Badge size="sm" color={statusCfg.color} type="pill-color">
|
||||
<FontAwesomeIcon icon={status === 'connected' ? faCircleCheck : faCircleXmark} className="mr-1 size-3" />
|
||||
{statusCfg.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{details.length > 0 && (
|
||||
@@ -89,6 +116,9 @@ const IntegrationCard = ({ name, description, icon, iconColor, status, details,
|
||||
|
||||
export const IntegrationsPage = () => {
|
||||
const webhookBase = 'https://engage-api.srv1477139.hstgr.cloud';
|
||||
const [editIntegration, setEditIntegration] = useState<IntegrationConfig | null>(null);
|
||||
|
||||
const openEdit = (config: IntegrationConfig) => setEditIntegration(config);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
@@ -110,6 +140,16 @@ export const IntegrationsPage = () => {
|
||||
{ label: 'DID Number', value: '+91 804 176 3265' },
|
||||
]}
|
||||
webhookUrl={`${webhookBase}/webhooks/ozonetel/missed-call`}
|
||||
onConfigure={() => openEdit({
|
||||
type: 'ozonetel',
|
||||
name: 'Ozonetel CloudAgent',
|
||||
details: [
|
||||
{ label: 'Account', value: 'global_healthx' },
|
||||
{ label: 'Agent ID', value: 'global' },
|
||||
{ label: 'SIP Extension', value: '523590' },
|
||||
{ label: 'Inbound Campaign', value: 'Inbound_918041763265' },
|
||||
],
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* WhatsApp */}
|
||||
@@ -120,6 +160,7 @@ export const IntegrationsPage = () => {
|
||||
iconColor="text-green-600"
|
||||
status="disconnected"
|
||||
details={[]}
|
||||
onConfigure={() => openEdit({ type: 'whatsapp', name: 'WhatsApp Business', details: [] })}
|
||||
/>
|
||||
|
||||
{/* Facebook Lead Ads */}
|
||||
@@ -130,6 +171,7 @@ export const IntegrationsPage = () => {
|
||||
iconColor="text-blue-600"
|
||||
status="disconnected"
|
||||
details={[]}
|
||||
onConfigure={() => openEdit({ type: 'facebook', name: 'Facebook Lead Ads', details: [] })}
|
||||
/>
|
||||
|
||||
{/* Google Ads */}
|
||||
@@ -140,6 +182,7 @@ export const IntegrationsPage = () => {
|
||||
iconColor="text-red-500"
|
||||
status="disconnected"
|
||||
details={[]}
|
||||
onConfigure={() => openEdit({ type: 'google', name: 'Google Ads', details: [] })}
|
||||
/>
|
||||
|
||||
{/* Instagram */}
|
||||
@@ -150,6 +193,7 @@ export const IntegrationsPage = () => {
|
||||
iconColor="text-pink-600"
|
||||
status="disconnected"
|
||||
details={[]}
|
||||
onConfigure={() => openEdit({ type: 'instagram', name: 'Instagram Lead Ads', details: [] })}
|
||||
/>
|
||||
|
||||
{/* Website */}
|
||||
@@ -163,6 +207,12 @@ export const IntegrationsPage = () => {
|
||||
{ label: 'Method', value: 'POST webhook' },
|
||||
]}
|
||||
webhookUrl={`${webhookBase}/webhooks/website/lead`}
|
||||
onConfigure={() => openEdit({
|
||||
type: 'website',
|
||||
name: 'Website Lead Forms',
|
||||
details: [{ label: 'Method', value: 'POST webhook' }],
|
||||
webhookUrl: `${webhookBase}/webhooks/website/lead`,
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Email */}
|
||||
@@ -173,8 +223,17 @@ export const IntegrationsPage = () => {
|
||||
iconColor="text-fg-quaternary"
|
||||
status="disconnected"
|
||||
details={[]}
|
||||
onConfigure={() => openEdit({ type: 'email', name: 'Email (SMTP)', details: [] })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editIntegration && (
|
||||
<IntegrationEditSlideout
|
||||
isOpen={!!editIntegration}
|
||||
onOpenChange={(open) => { if (!open) setEditIntegration(null); }}
|
||||
integration={editIntegration}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user