fix: wire role-based views — sidebar auth, user display, table tabs, card actions

- Sidebar: use useAuth() for isAdmin and pass auth user to NavAccountCard
- NavAccountCard: fix bug where items prop was ignored (used placeholderAccounts)
- TopBar: replace hardcoded "SM" initials with user.initials from auth
- All Leads: add "My Leads" tab filtering by assignedAgent matching user
- Lead Card: add role-aware action buttons (Call/Disposition for assigned leads)
- Lead Workspace: pass onLogCall/onUpdateStatus handlers to LeadCard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 16:15:08 +05:30
parent 8b796bf916
commit d98da9a1ea
6 changed files with 56 additions and 7 deletions

View File

@@ -167,7 +167,7 @@ export const NavAccountCard = ({
const triggerRef = useRef<HTMLDivElement>(null); const triggerRef = useRef<HTMLDivElement>(null);
const isDesktop = useBreakpoint("lg"); const isDesktop = useBreakpoint("lg");
const selectedAccount = placeholderAccounts.find((account) => account.id === selectedAccountId); const selectedAccount = items.find((account) => account.id === selectedAccountId);
if (!selectedAccount) { if (!selectedAccount) {
console.warn(`Account with ID ${selectedAccountId} not found in <NavAccountCard />`); console.warn(`Account with ID ${selectedAccountId} not found in <NavAccountCard />`);

View File

@@ -110,7 +110,17 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
{/* Account card */} {/* Account card */}
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4"> <div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
<NavAccountCard onSignOut={handleSignOut} /> <NavAccountCard
items={[{
id: 'current',
name: user.name,
email: user.email,
avatar: '',
status: 'online' as const,
}]}
selectedAccountId="current"
onSignOut={handleSignOut}
/>
</div> </div>
</aside> </aside>
); );

View File

@@ -1,6 +1,7 @@
import { SearchLg } from "@untitledui/icons"; import { SearchLg } from "@untitledui/icons";
import { Avatar } from "@/components/base/avatar/avatar"; import { Avatar } from "@/components/base/avatar/avatar";
import { Input } from "@/components/base/input/input"; import { Input } from "@/components/base/input/input";
import { useAuth } from "@/providers/auth-provider";
interface TopBarProps { interface TopBarProps {
title: string; title: string;
@@ -8,6 +9,8 @@ interface TopBarProps {
} }
export const TopBar = ({ title, subtitle }: TopBarProps) => { export const TopBar = ({ title, subtitle }: TopBarProps) => {
const { user } = useAuth();
return ( return (
<header className="flex h-16 items-center justify-between border-b border-secondary bg-primary px-6"> <header className="flex h-16 items-center justify-between border-b border-secondary bg-primary px-6">
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
@@ -23,7 +26,7 @@ export const TopBar = ({ title, subtitle }: TopBarProps) => {
aria-label="Search" aria-label="Search"
/> />
</div> </div>
<Avatar initials="SM" size="sm" /> <Avatar initials={user.initials} size="sm" />
</div> </div>
</header> </header>
); );

View File

@@ -14,6 +14,8 @@ interface LeadCardProps {
onMessage: (lead: Lead) => void; onMessage: (lead: Lead) => void;
onMarkSpam: (lead: Lead) => void; onMarkSpam: (lead: Lead) => void;
onMerge: (lead: Lead) => void; onMerge: (lead: Lead) => void;
onLogCall?: (lead: Lead) => void;
onUpdateStatus?: (lead: Lead) => void;
} }
const sourceLabelMap: Record<string, string> = { const sourceLabelMap: Record<string, string> = {
@@ -29,7 +31,7 @@ const sourceLabelMap: Record<string, string> = {
OTHER: 'Other', OTHER: 'Other',
}; };
export const LeadCard = ({ lead, onAssign, onMessage, onMarkSpam, onMerge }: LeadCardProps) => { export const LeadCard = ({ lead, onAssign, onMessage, onMarkSpam, onMerge, onLogCall, onUpdateStatus }: LeadCardProps) => {
const firstName = lead.contactName?.firstName ?? ''; const firstName = lead.contactName?.firstName ?? '';
const lastName = lead.contactName?.lastName ?? ''; const lastName = lead.contactName?.lastName ?? '';
const name = `${firstName} ${lastName}`.trim() || 'Unknown'; const name = `${firstName} ${lastName}`.trim() || 'Unknown';
@@ -38,6 +40,7 @@ export const LeadCard = ({ lead, onAssign, onMessage, onMarkSpam, onMerge }: Lea
const sourceLabel = lead.leadSource ? sourceLabelMap[lead.leadSource] ?? lead.leadSource : ''; const sourceLabel = lead.leadSource ? sourceLabelMap[lead.leadSource] ?? lead.leadSource : '';
const isSpam = (lead.spamScore ?? 0) >= 60; const isSpam = (lead.spamScore ?? 0) >= 60;
const isDuplicate = lead.isDuplicate === true; const isDuplicate = lead.isDuplicate === true;
const isAssigned = lead.assignedAgent !== null && lead.leadStatus !== 'NEW';
return ( return (
<div <div
@@ -99,6 +102,15 @@ export const LeadCard = ({ lead, onAssign, onMessage, onMarkSpam, onMerge }: Lea
Merge Merge
</Button> </Button>
</> </>
) : isAssigned ? (
<>
<Button size="sm" color="primary" onClick={() => onLogCall?.(lead)}>
Call
</Button>
<Button size="sm" color="secondary" onClick={() => onUpdateStatus?.(lead)}>
Disposition
</Button>
</>
) : ( ) : (
<> <>
<Button size="sm" color="primary" onClick={() => onAssign(lead)}> <Button size="sm" color="primary" onClick={() => onAssign(lead)}>

View File

@@ -9,9 +9,10 @@ import { LeadTable } from '@/components/leads/lead-table';
import { BulkActionBar } from '@/components/leads/bulk-action-bar'; import { BulkActionBar } from '@/components/leads/bulk-action-bar';
import { FilterPills } from '@/components/leads/filter-pills'; import { FilterPills } from '@/components/leads/filter-pills';
import { useLeads } from '@/hooks/use-leads'; import { useLeads } from '@/hooks/use-leads';
import { useAuth } from '@/providers/auth-provider';
import type { LeadSource, LeadStatus } from '@/types/entities'; import type { LeadSource, LeadStatus } from '@/types/entities';
type TabKey = 'new' | 'all'; type TabKey = 'new' | 'my-leads' | 'all';
type ActiveFilter = { type ActiveFilter = {
key: string; key: string;
@@ -22,6 +23,7 @@ type ActiveFilter = {
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
export const AllLeadsPage = () => { export const AllLeadsPage = () => {
const { user } = useAuth();
const [tab, setTab] = useState<TabKey>('new'); const [tab, setTab] = useState<TabKey>('new');
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [sortField, setSortField] = useState('createdAt'); const [sortField, setSortField] = useState('createdAt');
@@ -31,6 +33,7 @@ export const AllLeadsPage = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const statusFilter: LeadStatus | undefined = tab === 'new' ? 'NEW' : undefined; const statusFilter: LeadStatus | undefined = tab === 'new' ? 'NEW' : undefined;
const myLeadsOnly = tab === 'my-leads';
const { leads: filteredLeads, total } = useLeads({ const { leads: filteredLeads, total } = useLeads({
source: sourceFilter ?? undefined, source: sourceFilter ?? undefined,
@@ -93,9 +96,17 @@ export const AllLeadsPage = () => {
return sorted; return sorted;
}, [filteredLeads, sortField, sortDirection]); }, [filteredLeads, sortField, sortDirection]);
// Apply "My Leads" filter when on that tab
const displayLeads = useMemo(() => {
if (myLeadsOnly) {
return sortedLeads.filter((l) => l.assignedAgent === user.name);
}
return sortedLeads;
}, [sortedLeads, myLeadsOnly, user.name]);
// Client-side pagination // Client-side pagination
const totalPages = Math.max(1, Math.ceil(sortedLeads.length / PAGE_SIZE)); const totalPages = Math.max(1, Math.ceil(displayLeads.length / PAGE_SIZE));
const pagedLeads = sortedLeads.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); const pagedLeads = displayLeads.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
const handleSort = (field: string) => { const handleSort = (field: string) => {
if (field === sortField) { if (field === sortField) {
@@ -139,8 +150,11 @@ export const AllLeadsPage = () => {
setCurrentPage(1); setCurrentPage(1);
}; };
const myLeadsCount = sortedLeads.filter((l) => l.assignedAgent === user.name).length;
const tabItems = [ const tabItems = [
{ id: 'new', label: 'New', badge: tab === 'new' ? total : undefined }, { id: 'new', label: 'New', badge: tab === 'new' ? total : undefined },
{ id: 'my-leads', label: 'My Leads', badge: tab === 'my-leads' ? myLeadsCount : undefined },
{ id: 'all', label: 'All Leads', badge: tab === 'all' ? total : undefined }, { id: 'all', label: 'All Leads', badge: tab === 'all' ? total : undefined },
]; ];

View File

@@ -37,6 +37,14 @@ export const LeadWorkspacePage = () => {
// placeholder // placeholder
}; };
const handleLogCall = () => {
// placeholder
};
const handleUpdateStatus = () => {
// placeholder
};
return ( return (
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<TopBar title="Lead Workspace" subtitle="Ramaiah Memorial Hospital · Last 24 hours" /> <TopBar title="Lead Workspace" subtitle="Ramaiah Memorial Hospital · Last 24 hours" />
@@ -69,6 +77,8 @@ export const LeadWorkspacePage = () => {
onMessage={handleMessage} onMessage={handleMessage}
onMarkSpam={handleMarkSpam} onMarkSpam={handleMarkSpam}
onMerge={handleMerge} onMerge={handleMerge}
onLogCall={handleLogCall}
onUpdateStatus={handleUpdateStatus}
/> />
))} ))}
{displayLeads.length === 0 && ( {displayLeads.length === 0 && (