mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: 3-role auth, role-based routing, role-specific sidebar navigation
Add cc-agent role alongside executive and admin. Login page now has 3 tabs (Marketing Executive, Call Center, Admin). RoleRouter renders the appropriate home page per role. Sidebar shows completely different nav items per role with role subtitle. Placeholder pages added for Team Dashboard, Call Desk, Call History, and Follow-ups. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
src/components/layout/role-router.tsx
Normal file
17
src/components/layout/role-router.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { LeadWorkspacePage } from '@/pages/lead-workspace';
|
||||
import { TeamDashboardPage } from '@/pages/team-dashboard';
|
||||
import { CallDeskPage } from '@/pages/call-desk';
|
||||
|
||||
export const RoleRouter = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
switch (user.role) {
|
||||
case 'admin':
|
||||
return <TeamDashboardPage />;
|
||||
case 'cc-agent':
|
||||
return <CallDeskPage />;
|
||||
default:
|
||||
return <LeadWorkspacePage />;
|
||||
}
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBell,
|
||||
faBullhorn,
|
||||
faChartMixed,
|
||||
faClockRotateLeft,
|
||||
faCommentDots,
|
||||
faGear,
|
||||
faGrid2,
|
||||
faPhone,
|
||||
faPlug,
|
||||
faUsers,
|
||||
} from "@fortawesome/pro-duotone-svg-icons";
|
||||
import { useNavigate } from "react-router";
|
||||
import { MobileNavigationHeader } from "@/components/application/app-navigation/base-components/mobile-header";
|
||||
@@ -36,28 +40,100 @@ const IconPlug: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
const IconGear: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faGear} className={className} />
|
||||
);
|
||||
const IconPhone: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faPhone} className={className} />
|
||||
);
|
||||
const IconBell: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faBell} className={className} />
|
||||
);
|
||||
const IconClockRewind: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} className={className} />
|
||||
);
|
||||
const IconUsers: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faUsers} className={className} />
|
||||
);
|
||||
|
||||
const mainItems: NavItemType[] = [
|
||||
{ label: "Lead Workspace", href: "/", icon: IconGrid2 },
|
||||
{ label: "Campaigns", href: "/campaigns", icon: IconBullhorn },
|
||||
{ label: "Outreach", href: "/outreach", icon: IconCommentDots },
|
||||
];
|
||||
type NavSection = {
|
||||
label: string;
|
||||
items: NavItemType[];
|
||||
};
|
||||
|
||||
const insightsItems: NavItemType[] = [
|
||||
{ label: "Analytics", href: "/analytics", icon: IconChartMixed },
|
||||
];
|
||||
const getNavSections = (role: string): NavSection[] => {
|
||||
if (role === 'admin') {
|
||||
return [
|
||||
{
|
||||
label: 'Overview',
|
||||
items: [
|
||||
{ label: 'Team Dashboard', href: '/', icon: IconGrid2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Management',
|
||||
items: [
|
||||
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
|
||||
{ label: 'Analytics', href: '/analytics', icon: IconChartMixed },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
items: [
|
||||
{ label: 'Integrations', href: '/integrations', icon: IconPlug },
|
||||
{ label: 'Settings', href: '/settings', icon: IconGear },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const adminItems: NavItemType[] = [
|
||||
{ label: "Integrations", href: "/integrations", icon: IconPlug },
|
||||
{ label: "Settings", href: "/settings", icon: IconGear },
|
||||
];
|
||||
if (role === 'cc-agent') {
|
||||
return [
|
||||
{
|
||||
label: 'Call Center',
|
||||
items: [
|
||||
{ label: 'Call Desk', href: '/', icon: IconPhone },
|
||||
{ label: 'Follow-ups', href: '/follow-ups', icon: IconBell },
|
||||
{ label: 'Call History', href: '/call-history', icon: IconClockRewind },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Executive (default)
|
||||
return [
|
||||
{
|
||||
label: 'Main',
|
||||
items: [
|
||||
{ label: 'Lead Workspace', href: '/', icon: IconGrid2 },
|
||||
{ label: 'All Leads', href: '/leads', icon: IconUsers },
|
||||
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
|
||||
{ label: 'Outreach', href: '/outreach', icon: IconCommentDots },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Insights',
|
||||
items: [
|
||||
{ label: 'Analytics', href: '/analytics', icon: IconChartMixed },
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getRoleSubtitle = (role: string): string => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return 'Marketing Admin';
|
||||
case 'cc-agent':
|
||||
return 'Call Center Agent';
|
||||
default:
|
||||
return 'Marketing Executive';
|
||||
}
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
activeUrl?: string;
|
||||
}
|
||||
|
||||
export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
const { logout, isAdmin: authIsAdmin, user } = useAuth();
|
||||
const { logout, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSignOut = () => {
|
||||
@@ -65,11 +141,7 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const navSections = [
|
||||
{ label: "Main", items: mainItems },
|
||||
{ label: "Insights", items: insightsItems },
|
||||
...(authIsAdmin ? [{ label: "Admin", items: adminItems }] : []),
|
||||
];
|
||||
const navSections = getNavSections(user.role);
|
||||
|
||||
const content = (
|
||||
<aside
|
||||
@@ -79,7 +151,7 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
{/* Logo */}
|
||||
<div className="flex flex-col gap-1 px-4 lg:px-5">
|
||||
<span className="text-md font-bold text-primary">Helix Engage</span>
|
||||
<span className="text-xs text-tertiary">Ramaiah Memorial Hospital</span>
|
||||
<span className="text-xs text-tertiary">Ramaiah Memorial Hospital · {getRoleSubtitle(user.role)}</span>
|
||||
</div>
|
||||
|
||||
{/* Nav sections */}
|
||||
|
||||
12
src/main.tsx
12
src/main.tsx
@@ -3,13 +3,17 @@ import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
|
||||
import { AppShell } from "@/components/layout/app-shell";
|
||||
import { AuthGuard } from "@/components/layout/auth-guard";
|
||||
import { RoleRouter } from "@/components/layout/role-router";
|
||||
import { NotFound } from "@/pages/not-found";
|
||||
import { AllLeadsPage } from "@/pages/all-leads";
|
||||
import { CallDeskPage } from "@/pages/call-desk";
|
||||
import { CallHistoryPage } from "@/pages/call-history";
|
||||
import { CampaignDetailPage } from "@/pages/campaign-detail";
|
||||
import { CampaignsPage } from "@/pages/campaigns";
|
||||
import { LeadWorkspacePage } from "@/pages/lead-workspace";
|
||||
import { FollowUpsPage } from "@/pages/follow-ups-page";
|
||||
import { LoginPage } from "@/pages/login";
|
||||
import { OutreachPage } from "@/pages/outreach";
|
||||
import { TeamDashboardPage } from "@/pages/team-dashboard";
|
||||
import { AuthProvider } from "@/providers/auth-provider";
|
||||
import { DataProvider } from "@/providers/data-provider";
|
||||
import { RouteProvider } from "@/providers/router-provider";
|
||||
@@ -33,11 +37,15 @@ createRoot(document.getElementById("root")!).render(
|
||||
</AppShell>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<LeadWorkspacePage />} />
|
||||
<Route path="/" element={<RoleRouter />} />
|
||||
<Route path="/leads" element={<AllLeadsPage />} />
|
||||
<Route path="/campaigns" element={<CampaignsPage />} />
|
||||
<Route path="/campaigns/:id" element={<CampaignDetailPage />} />
|
||||
<Route path="/outreach" element={<OutreachPage />} />
|
||||
<Route path="/follow-ups" element={<FollowUpsPage />} />
|
||||
<Route path="/call-history" element={<CallHistoryPage />} />
|
||||
<Route path="/call-desk" element={<CallDeskPage />} />
|
||||
<Route path="/team-dashboard" element={<TeamDashboardPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
15
src/pages/call-desk.tsx
Normal file
15
src/pages/call-desk.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
|
||||
export const CallDeskPage = () => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<TopBar title="Call Desk" subtitle="Manage inbound and outbound calls" />
|
||||
<div className="flex flex-1 items-center justify-center p-7">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-display-xs font-bold text-primary">Call Desk</h2>
|
||||
<p className="text-sm text-tertiary">Coming soon — call queue, dialer, and live call management.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
15
src/pages/call-history.tsx
Normal file
15
src/pages/call-history.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
|
||||
export const CallHistoryPage = () => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<TopBar title="Call History" subtitle="Past call logs and recordings" />
|
||||
<div className="flex flex-1 items-center justify-center p-7">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-display-xs font-bold text-primary">Call History</h2>
|
||||
<p className="text-sm text-tertiary">Coming soon — call logs, recordings, and outcome tracking.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
15
src/pages/follow-ups-page.tsx
Normal file
15
src/pages/follow-ups-page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
|
||||
export const FollowUpsPage = () => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<TopBar title="Follow-ups" subtitle="Scheduled follow-up tasks" />
|
||||
<div className="flex flex-1 items-center justify-center p-7">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-display-xs font-bold text-primary">Follow-ups</h2>
|
||||
<p className="text-sm text-tertiary">Coming soon — follow-up reminders, scheduling, and task management.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ const features = [
|
||||
},
|
||||
];
|
||||
|
||||
type RoleTab = 'executive' | 'admin';
|
||||
type RoleTab = 'executive' | 'cc-agent' | 'admin';
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { login, setRole } = useAuth();
|
||||
@@ -124,30 +124,25 @@ export const LoginPage = () => {
|
||||
<div
|
||||
className="mt-8 flex items-center gap-1 rounded-xl p-1 bg-secondary"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabChange('executive')}
|
||||
className={[
|
||||
'flex-1 rounded-lg px-3 py-2 text-sm font-semibold transition duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||
activeTab === 'executive'
|
||||
? 'bg-primary text-brand-secondary shadow-sm'
|
||||
: 'text-tertiary hover:text-secondary',
|
||||
].join(' ')}
|
||||
>
|
||||
Marketing Executive
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabChange('admin')}
|
||||
className={[
|
||||
'flex-1 rounded-lg px-3 py-2 text-sm font-semibold transition duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||
activeTab === 'admin'
|
||||
? 'bg-primary text-brand-secondary shadow-sm'
|
||||
: 'text-tertiary hover:text-secondary',
|
||||
].join(' ')}
|
||||
>
|
||||
Admin
|
||||
</button>
|
||||
{([
|
||||
{ key: 'executive' as const, label: 'Marketing Executive' },
|
||||
{ key: 'cc-agent' as const, label: 'Call Center' },
|
||||
{ key: 'admin' as const, label: 'Admin' },
|
||||
]).map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => handleTabChange(tab.key)}
|
||||
className={[
|
||||
'flex-1 rounded-lg px-3 py-2 text-sm font-semibold transition duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||
activeTab === tab.key
|
||||
? 'bg-primary text-brand-secondary shadow-sm'
|
||||
: 'text-tertiary hover:text-secondary',
|
||||
].join(' ')}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Google sign-in */}
|
||||
|
||||
15
src/pages/team-dashboard.tsx
Normal file
15
src/pages/team-dashboard.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
|
||||
export const TeamDashboardPage = () => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<TopBar title="Team Dashboard" subtitle="Team performance overview" />
|
||||
<div className="flex flex-1 items-center justify-center p-7">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h2 className="text-display-xs font-bold text-primary">Team Dashboard</h2>
|
||||
<p className="text-sm text-tertiary">Coming soon — team performance metrics and management tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
export type Role = 'executive' | 'admin';
|
||||
export type Role = 'executive' | 'admin' | 'cc-agent';
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
@@ -14,6 +14,7 @@ type AuthContextType = {
|
||||
user: User;
|
||||
setRole: (role: Role) => void;
|
||||
isAdmin: boolean;
|
||||
isCCAgent: boolean;
|
||||
isAuthenticated: boolean;
|
||||
login: () => void;
|
||||
logout: () => void;
|
||||
@@ -32,6 +33,12 @@ const USERS: Record<Role, User> = {
|
||||
role: 'executive',
|
||||
email: 'sanjay@ramaiah.com',
|
||||
},
|
||||
'cc-agent': {
|
||||
name: 'Rekha S.',
|
||||
initials: 'RS',
|
||||
role: 'cc-agent' as const,
|
||||
email: 'rekha@ramaiah.com',
|
||||
},
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
@@ -56,12 +63,13 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
|
||||
const user = USERS[role];
|
||||
const isAdmin = role === 'admin';
|
||||
const isCCAgent = role === 'cc-agent';
|
||||
|
||||
const login = () => setIsAuthenticated(true);
|
||||
const logout = () => setIsAuthenticated(false);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, setRole, isAdmin, isAuthenticated, login, logout }}>
|
||||
<AuthContext.Provider value={{ user, setRole, isAdmin, isCCAgent, isAuthenticated, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user