diff --git a/src/components/call-desk/worklist-panel.tsx b/src/components/call-desk/worklist-panel.tsx
index ca08fa4..5bacbd3 100644
--- a/src/components/call-desk/worklist-panel.tsx
+++ b/src/components/call-desk/worklist-panel.tsx
@@ -62,7 +62,7 @@ interface WorklistPanelProps {
selectedLeadId: string | null;
}
-type TabKey = 'all' | 'missed' | 'callbacks' | 'follow-ups';
+type TabKey = 'all' | 'missed' | 'leads' | 'follow-ups';
type WorklistRow = {
id: string;
@@ -258,7 +258,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
const filteredRows = useMemo(() => {
let rows = allRows;
if (tab === 'missed') rows = missedSubTabRows;
- else if (tab === 'callbacks') rows = rows.filter((r) => r.type === 'callback');
+ else if (tab === 'leads') rows = rows.filter((r) => r.type === 'lead');
else if (tab === 'follow-ups') rows = rows.filter((r) => r.type === 'follow-up');
if (search.trim()) {
@@ -272,7 +272,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
}, [allRows, tab, search]);
const missedCount = allRows.filter((r) => r.type === 'missed').length;
- const callbackCount = allRows.filter((r) => r.type === 'callback').length;
+ const leadCount = allRows.filter((r) => r.type === 'lead').length;
const followUpCount = allRows.filter((r) => r.type === 'follow-up').length;
// Notification for new missed calls
@@ -296,7 +296,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
const tabItems = [
{ id: 'all' as const, label: 'All Tasks', badge: allRows.length > 0 ? String(allRows.length) : undefined },
{ id: 'missed' as const, label: 'Missed Calls', badge: missedCount > 0 ? String(missedCount) : undefined },
- { id: 'callbacks' as const, label: 'Callbacks', badge: callbackCount > 0 ? String(callbackCount) : undefined },
+ { id: 'leads' as const, label: 'Leads', badge: leadCount > 0 ? String(leadCount) : undefined },
{ id: 'follow-ups' as const, label: 'Follow-ups', badge: followUpCount > 0 ? String(followUpCount) : undefined },
];
@@ -378,7 +378,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
-
+
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx
index 10feaf4..5ce7b5e 100644
--- a/src/components/layout/sidebar.tsx
+++ b/src/components/layout/sidebar.tsx
@@ -70,6 +70,7 @@ const getNavSections = (role: string): NavSection[] => {
{ label: 'Call Center', items: [
{ label: 'Call Desk', href: '/', icon: IconPhone },
{ label: 'Call History', href: '/call-history', icon: IconClockRewind },
+ { label: 'Patients', href: '/patients', icon: IconHospitalUser },
{ label: 'My Performance', href: '/my-performance', icon: IconChartMixed },
]},
];
diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts
index cf471a9..3d44bee 100644
--- a/src/lib/api-client.ts
+++ b/src/lib/api-client.ts
@@ -31,8 +31,55 @@ const authHeaders = (): Record => {
};
};
-// Shared response handler — extracts error message, handles 401, toasts on failure
-const handleResponse = async (response: Response, silent = false): Promise => {
+// Token refresh — attempts to get a new access token using the refresh token
+let refreshPromise: Promise | null = null;
+
+const tryRefreshToken = async (): Promise => {
+ // Deduplicate concurrent refresh attempts
+ if (refreshPromise) return refreshPromise;
+
+ refreshPromise = (async () => {
+ const refreshToken = getRefreshToken();
+ if (!refreshToken) return false;
+
+ try {
+ const response = await fetch(`${API_URL}/auth/refresh`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ refreshToken }),
+ });
+
+ if (!response.ok) return false;
+
+ const data = await response.json();
+ if (data.accessToken && data.refreshToken) {
+ storeTokens(data.accessToken, data.refreshToken);
+ return true;
+ }
+ return false;
+ } catch {
+ return false;
+ } finally {
+ refreshPromise = null;
+ }
+ })();
+
+ return refreshPromise;
+};
+
+// Shared response handler — extracts error message, handles 401 with auto-refresh, toasts on failure
+const handleResponse = async (response: Response, silent = false, retryFn?: () => Promise): Promise => {
+ if (response.status === 401 && retryFn) {
+ const refreshed = await tryRefreshToken();
+ if (refreshed) {
+ const retryResponse = await retryFn();
+ return handleResponse(retryResponse, silent);
+ }
+ clearTokens();
+ if (!silent) notify.error('Session expired. Please log in again.');
+ throw new AuthError();
+ }
+
if (response.status === 401) {
clearTokens();
if (!silent) notify.error('Session expired. Please log in again.');
@@ -86,16 +133,23 @@ export const apiClient = {
const token = getStoredToken();
if (!token) throw new AuthError();
- const response = await fetch(`${API_URL}/graphql`, {
+ const doFetch = () => fetch(`${API_URL}/graphql`, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ query, variables }),
});
+ let response = await doFetch();
+
if (response.status === 401) {
- clearTokens();
- if (!options?.silent) notify.error('Session expired', 'Please log in again.');
- throw new AuthError();
+ const refreshed = await tryRefreshToken();
+ if (refreshed) {
+ response = await doFetch();
+ } else {
+ clearTokens();
+ if (!options?.silent) notify.error('Session expired', 'Please log in again.');
+ throw new AuthError();
+ }
}
const json = await response.json();
@@ -110,20 +164,22 @@ export const apiClient = {
// REST — all sidecar API calls go through these
async post(path: string, body?: Record, options?: { silent?: boolean }): Promise {
- const response = await fetch(`${API_URL}${path}`, {
+ const doFetch = () => fetch(`${API_URL}${path}`, {
method: 'POST',
headers: authHeaders(),
body: body ? JSON.stringify(body) : undefined,
});
- return handleResponse(response, options?.silent);
+ const response = await doFetch();
+ return handleResponse(response, options?.silent, doFetch);
},
async get(path: string, options?: { silent?: boolean }): Promise {
- const response = await fetch(`${API_URL}${path}`, {
+ const doFetch = () => fetch(`${API_URL}${path}`, {
method: 'GET',
headers: authHeaders(),
});
- return handleResponse(response, options?.silent);
+ const response = await doFetch();
+ return handleResponse(response, options?.silent, doFetch);
},
// Health check — silent, no toasts
diff --git a/src/pages/call-desk.tsx b/src/pages/call-desk.tsx
index f9fac0f..5f9b2cc 100644
--- a/src/pages/call-desk.tsx
+++ b/src/pages/call-desk.tsx
@@ -22,8 +22,14 @@ export const CallDeskPage = () => {
const [selectedLead, setSelectedLead] = useState(null);
const [contextOpen, setContextOpen] = useState(true);
const [activeMissedCallId, setActiveMissedCallId] = useState(null);
+ const [callDismissed, setCallDismissed] = useState(false);
- const isInCall = callState === 'ringing-in' || callState === 'ringing-out' || callState === 'active' || callState === 'ended' || callState === 'failed';
+ // Reset callDismissed when a new call starts (ringing in or out)
+ if (callDismissed && (callState === 'ringing-in' || callState === 'ringing-out')) {
+ setCallDismissed(false);
+ }
+
+ const isInCall = !callDismissed && (callState === 'ringing-in' || callState === 'ringing-out' || callState === 'active' || callState === 'ended' || callState === 'failed');
const callerLead = callerNumber
? marketingLeads.find((l) => l.contactPhone?.[0]?.number?.endsWith(callerNumber) || callerNumber.endsWith(l.contactPhone?.[0]?.number ?? '---'))
@@ -63,7 +69,7 @@ export const CallDeskPage = () => {
{/* Active call */}
{isInCall && (
-
setActiveMissedCallId(null)} />
+ { setActiveMissedCallId(null); setCallDismissed(true); }} />
)}
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
index 0310525..c4f0abf 100644
--- a/src/pages/login.tsx
+++ b/src/pages/login.tsx
@@ -165,7 +165,7 @@ export const LoginPage = () => {