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,5 +1,5 @@
import type { ReactNode } from 'react';
import { createContext, useCallback, useContext, useState } from 'react';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
export type Role = 'executive' | 'admin' | 'cc-agent';
@@ -18,6 +18,7 @@ type AuthContextType = {
isAdmin: boolean;
isCCAgent: boolean;
isAuthenticated: boolean;
loading: boolean;
loginWithUser: (userData: User) => void;
login: () => void;
logout: () => void;
@@ -34,6 +35,21 @@ const DEFAULT_USER: User = {
const getInitials = (firstName: string, lastName: string): string =>
`${firstName[0] ?? ''}${lastName[0] ?? ''}`.toUpperCase();
const STORAGE_KEY = 'helix_user';
const loadPersistedUser = (): User | null => {
try {
const token = localStorage.getItem('helix_access_token');
const stored = localStorage.getItem(STORAGE_KEY);
if (token && stored) {
return JSON.parse(stored) as User;
}
} catch {
// corrupt data
}
return null;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = (): AuthContextType => {
@@ -49,19 +65,32 @@ interface AuthProviderProps {
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<User>(DEFAULT_USER);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const persisted = loadPersistedUser();
const [user, setUser] = useState<User>(persisted ?? DEFAULT_USER);
const [isAuthenticated, setIsAuthenticated] = useState(!!persisted);
const [loading, setLoading] = useState(!persisted && !!localStorage.getItem('helix_access_token'));
// If we have a token but no persisted user, try to restore session
useEffect(() => {
if (!isAuthenticated && localStorage.getItem('helix_access_token') && !persisted) {
// Token exists but no user data — could re-fetch profile here
// For now, just clear stale token
localStorage.removeItem('helix_access_token');
localStorage.removeItem('helix_refresh_token');
localStorage.removeItem(STORAGE_KEY);
setLoading(false);
}
}, [isAuthenticated, persisted]);
const isAdmin = user.role === 'admin';
const isCCAgent = user.role === 'cc-agent';
// Real login — receives user profile from sidecar auth response
const loginWithUser = useCallback((userData: User) => {
setUser(userData);
setIsAuthenticated(true);
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
}, []);
// Simple login (for backward compat)
const login = useCallback(() => {
setIsAuthenticated(true);
}, []);
@@ -71,14 +100,19 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
setIsAuthenticated(false);
localStorage.removeItem('helix_access_token');
localStorage.removeItem('helix_refresh_token');
localStorage.removeItem(STORAGE_KEY);
}, []);
const setRole = useCallback((role: Role) => {
setUser(prev => ({ ...prev, role }));
setUser(prev => {
const updated = { ...prev, role };
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return updated;
});
}, []);
return (
<AuthContext.Provider value={{ user, isAdmin, isCCAgent, isAuthenticated, loginWithUser, login, logout, setRole }}>
<AuthContext.Provider value={{ user, isAdmin, isCCAgent, isAuthenticated, loading, loginWithUser, login, logout, setRole }}>
{children}
</AuthContext.Provider>
);