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:
172
src/components/campaigns/campaign-edit-slideout.tsx
Normal file
172
src/components/campaigns/campaign-edit-slideout.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPenToSquare } 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 { Select } from '@/components/base/select/select';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { notify } from '@/lib/toast';
|
||||
import type { Campaign, CampaignStatus } from '@/types/entities';
|
||||
|
||||
const PenIcon: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faPenToSquare} className={className} />
|
||||
);
|
||||
|
||||
type CampaignEditSlideoutProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
campaign: Campaign;
|
||||
onSaved?: () => void;
|
||||
};
|
||||
|
||||
const statusItems = [
|
||||
{ id: 'DRAFT' as const, label: 'Draft' },
|
||||
{ id: 'ACTIVE' as const, label: 'Active' },
|
||||
{ id: 'PAUSED' as const, label: 'Paused' },
|
||||
{ id: 'COMPLETED' as const, label: 'Completed' },
|
||||
];
|
||||
|
||||
const formatDateForInput = (dateStr: string | null): string => {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
return new Date(dateStr).toISOString().slice(0, 10);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const budgetToDisplay = (campaign: Campaign): string => {
|
||||
if (!campaign.budget) return '';
|
||||
return String(Math.round(campaign.budget.amountMicros / 1_000_000));
|
||||
};
|
||||
|
||||
export const CampaignEditSlideout = ({ isOpen, onOpenChange, campaign, onSaved }: CampaignEditSlideoutProps) => {
|
||||
const [campaignName, setCampaignName] = useState(campaign.campaignName ?? '');
|
||||
const [status, setStatus] = useState<CampaignStatus | null>(campaign.campaignStatus);
|
||||
const [budget, setBudget] = useState(budgetToDisplay(campaign));
|
||||
const [startDate, setStartDate] = useState(formatDateForInput(campaign.startDate));
|
||||
const [endDate, setEndDate] = useState(formatDateForInput(campaign.endDate));
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSave = async (close: () => void) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const budgetMicros = budget ? Number(budget) * 1_000_000 : null;
|
||||
|
||||
await apiClient.graphql(
|
||||
`mutation UpdateCampaign($id: ID!, $data: CampaignUpdateInput!) {
|
||||
updateCampaign(id: $id, data: $data) { id }
|
||||
}`,
|
||||
{
|
||||
id: campaign.id,
|
||||
data: {
|
||||
campaignName: campaignName || null,
|
||||
campaignStatus: status,
|
||||
...(budgetMicros !== null
|
||||
? {
|
||||
budget: {
|
||||
amountMicros: budgetMicros,
|
||||
currencyCode: campaign.budget?.currencyCode ?? 'INR',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
startDate: startDate ? new Date(startDate).toISOString() : null,
|
||||
endDate: endDate ? new Date(endDate).toISOString() : null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
notify.success('Campaign updated', `${campaignName || 'Campaign'} has been updated successfully.`);
|
||||
onSaved?.();
|
||||
close();
|
||||
} catch (err) {
|
||||
// apiClient.graphql already toasts on error
|
||||
console.error('Failed to update campaign:', err);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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-brand-secondary">
|
||||
<PenIcon className="size-5 text-fg-brand-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-primary">Edit Campaign</h2>
|
||||
<p className="text-sm text-tertiary">Update campaign details</p>
|
||||
</div>
|
||||
</div>
|
||||
</SlideoutMenu.Header>
|
||||
|
||||
<SlideoutMenu.Content>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Campaign Name"
|
||||
placeholder="Enter campaign name"
|
||||
value={campaignName}
|
||||
onChange={setCampaignName}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Status"
|
||||
placeholder="Select status"
|
||||
items={statusItems}
|
||||
selectedKey={status}
|
||||
onSelectionChange={(key) => setStatus(key as CampaignStatus)}
|
||||
>
|
||||
{(item) => <Select.Item id={item.id} label={item.label} />}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
label="Budget (INR)"
|
||||
placeholder="e.g. 50000"
|
||||
type="number"
|
||||
value={budget}
|
||||
onChange={setBudget}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
label="Start Date"
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
/>
|
||||
<Input
|
||||
label="End Date"
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SlideoutMenu.Content>
|
||||
|
||||
<SlideoutMenu.Footer>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button size="md" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
color="primary"
|
||||
isLoading={isSaving}
|
||||
showTextWhileLoading
|
||||
onClick={() => handleSave(close)}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</SlideoutMenu.Footer>
|
||||
</>
|
||||
)}
|
||||
</SlideoutMenu>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user