feat: dashboard restructure, integrations, settings, UI fixes

Dashboard:
- Split into components (kpi-cards, agent-table, missed-queue)
- Add collapsible AI panel on right (same pattern as Call Desk)
- Add tabs: Agent Performance | Missed Queue | Campaigns
- Date range filter in header

Integrations page:
- Ozonetel (connected), WhatsApp, Facebook, Google, Instagram, Website, Email
- Status badges, config details, webhook URL with copy button

Settings page:
- Employee table from workspaceMembers GraphQL query
- Name, email, roles, status, reset password action

Fixes:
- Fix CALLS_QUERY: callerNumber needs { primaryPhoneNumber }, recordingUrl → recording { primaryLinkUrl }
- Remove duplicate AI Assistant header
- Remove Follow-ups from CC agent sidebar (already in worklist tabs)
- Remove global search from TopBar (decorative, unused)
- Slim down TopBar height
- Fix search/table gap in worklist
- Add brand border to active nav item

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 15:58:31 +05:30
parent 94f4a18035
commit d9d98bce9c
15 changed files with 805 additions and 521 deletions

130
src/pages/settings.tsx Normal file
View File

@@ -0,0 +1,130 @@
import { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faKey, faToggleOn, faToggleOff } from '@fortawesome/pro-duotone-svg-icons';
import { Avatar } from '@/components/base/avatar/avatar';
import { Badge } from '@/components/base/badges/badges';
import { Button } from '@/components/base/buttons/button';
import { Table, TableCard } from '@/components/application/table/table';
import { TopBar } from '@/components/layout/top-bar';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import { getInitials } from '@/lib/format';
type WorkspaceMember = {
id: string;
name: { firstName: string; lastName: string } | null;
userEmail: string;
avatarUrl: string | null;
roles: { id: string; label: string }[];
};
export const SettingsPage = () => {
const [members, setMembers] = useState<WorkspaceMember[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchMembers = async () => {
try {
const data = await apiClient.graphql<any>(
`{ workspaceMembers(first: 50) { edges { node { id name { firstName lastName } userEmail avatarUrl roles { id label } } } } }`,
undefined,
{ silent: true },
);
setMembers(data?.workspaceMembers?.edges?.map((e: any) => e.node) ?? []);
} catch {
// silently fail
} finally {
setLoading(false);
}
};
fetchMembers();
}, []);
const handleResetPassword = (member: WorkspaceMember) => {
notify.info('Password Reset', `Password reset link would be sent to ${member.userEmail}`);
};
return (
<div className="flex flex-1 flex-col overflow-hidden">
<TopBar title="Settings" subtitle="Team management and configuration" />
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* Employees section */}
<TableCard.Root size="sm">
<TableCard.Header
title="Employees"
badge={members.length}
description="Manage team members and their roles"
/>
{loading ? (
<div className="flex items-center justify-center py-12">
<p className="text-sm text-tertiary">Loading employees...</p>
</div>
) : (
<Table>
<Table.Header>
<Table.Head label="EMPLOYEE" />
<Table.Head label="EMAIL" />
<Table.Head label="ROLES" />
<Table.Head label="STATUS" />
<Table.Head label="ACTIONS" />
</Table.Header>
<Table.Body items={members}>
{(member) => {
const firstName = member.name?.firstName ?? '';
const lastName = member.name?.lastName ?? '';
const fullName = `${firstName} ${lastName}`.trim() || 'Unnamed';
const initials = getInitials(firstName || '?', lastName || '?');
const roles = member.roles?.map((r) => r.label) ?? [];
return (
<Table.Row id={member.id}>
<Table.Cell>
<div className="flex items-center gap-3">
<Avatar size="sm" initials={initials} src={member.avatarUrl ?? undefined} />
<span className="text-sm font-medium text-primary">{fullName}</span>
</div>
</Table.Cell>
<Table.Cell>
<span className="text-sm text-tertiary">{member.userEmail}</span>
</Table.Cell>
<Table.Cell>
<div className="flex flex-wrap gap-1">
{roles.length > 0 ? roles.map((role) => (
<Badge key={role} size="sm" color={role.includes('Manager') ? 'brand' : 'gray'}>
{role}
</Badge>
)) : (
<span className="text-xs text-quaternary">No roles</span>
)}
</div>
</Table.Cell>
<Table.Cell>
<Badge size="sm" color="success" type="pill-color">
<FontAwesomeIcon icon={faToggleOn} className="mr-1 size-3" />
Active
</Badge>
</Table.Cell>
<Table.Cell>
<Button
size="sm"
color="secondary"
iconLeading={({ className }: { className?: string }) => (
<FontAwesomeIcon icon={faKey} className={className} />
)}
onClick={() => handleResetPassword(member)}
>
Reset Password
</Button>
</Table.Cell>
</Table.Row>
);
}}
</Table.Body>
</Table>
)}
</TableCard.Root>
</div>
</div>
);
};