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:
2026-03-19 16:32:58 +05:30
parent bb004744f4
commit 567f9f2d72
8 changed files with 833 additions and 24 deletions

View File

@@ -1,13 +1,18 @@
import { useMemo, useState } from 'react';
import { Link } from 'react-router';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPenToSquare } from '@fortawesome/pro-duotone-svg-icons';
import { TopBar } from '@/components/layout/top-bar';
import { Tabs, TabList, Tab, TabPanel } from '@/components/application/tabs/tabs';
import { CampaignCard } from '@/components/campaigns/campaign-card';
import { CampaignEditSlideout } from '@/components/campaigns/campaign-edit-slideout';
import { Button } from '@/components/base/buttons/button';
import { useCampaigns } from '@/hooks/use-campaigns';
import { useLeads } from '@/hooks/use-leads';
import { useData } from '@/providers/data-provider';
import { formatCurrency } from '@/lib/format';
import type { CampaignStatus } from '@/types/entities';
import type { Campaign, CampaignStatus } from '@/types/entities';
type TabConfig = {
id: string;
@@ -25,7 +30,9 @@ const tabs: TabConfig[] = [
export const CampaignsPage = () => {
const [activeTab, setActiveTab] = useState<string>('all');
const [editCampaign, setEditCampaign] = useState<Campaign | null>(null);
const { refresh } = useData();
const selectedTab = tabs.find((t) => t.id === activeTab) ?? tabs[0];
const { campaigns, ads } = useCampaigns({ status: selectedTab.status });
const { campaigns: allCampaigns } = useCampaigns();
@@ -100,13 +107,30 @@ export const CampaignsPage = () => {
<TabPanel key={tab.id} id={tab.id}>
<div className="mt-5 grid grid-cols-1 gap-4 xl:grid-cols-2">
{campaigns.map((campaign) => (
<Link key={campaign.id} to={`/campaigns/${campaign.id}`} className="no-underline">
<CampaignCard
campaign={campaign}
ads={adsByCampaign.get(campaign.id) ?? []}
leads={leadsByCampaign.get(campaign.id) ?? []}
/>
</Link>
<div key={campaign.id} className="relative">
<Link to={`/campaigns/${campaign.id}`} className="no-underline">
<CampaignCard
campaign={campaign}
ads={adsByCampaign.get(campaign.id) ?? []}
leads={leadsByCampaign.get(campaign.id) ?? []}
/>
</Link>
<div className="absolute top-4 right-14">
<Button
size="sm"
color="secondary"
iconLeading={({ className }: { className?: string }) => (
<FontAwesomeIcon icon={faPenToSquare} className={className} />
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setEditCampaign(campaign);
}}
aria-label={`Edit ${campaign.campaignName ?? 'campaign'}`}
/>
</div>
</div>
))}
{campaigns.length === 0 && (
<p className="col-span-full py-12 text-center text-sm text-tertiary">
@@ -118,6 +142,15 @@ export const CampaignsPage = () => {
))}
</Tabs>
</div>
{editCampaign && (
<CampaignEditSlideout
isOpen={!!editCampaign}
onOpenChange={(open) => { if (!open) setEditCampaign(null); }}
campaign={editCampaign}
onSaved={refresh}
/>
)}
</div>
);
};