mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: QA fixes — Patient 360 rewrite, token refresh, call flow, UI polish
- Patient 360 page queries Patient entity with appointments, calls, leads - Patients added to CC agent sidebar navigation - Auto token refresh on 401 (deduplicated concurrent refreshes) - Call desk: callDismissed flag prevents SIP race on worklist return - Missed calls skip disposition when never answered - Callbacks tab renamed to Leads tab - Branch column header on missed calls tab - F0rty2.ai link on login footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,8 +31,55 @@ const authHeaders = (): Record<string, string> => {
|
||||
};
|
||||
};
|
||||
|
||||
// Shared response handler — extracts error message, handles 401, toasts on failure
|
||||
const handleResponse = async <T>(response: Response, silent = false): Promise<T> => {
|
||||
// Token refresh — attempts to get a new access token using the refresh token
|
||||
let refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
const tryRefreshToken = async (): Promise<boolean> => {
|
||||
// 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 <T>(response: Response, silent = false, retryFn?: () => Promise<Response>): Promise<T> => {
|
||||
if (response.status === 401 && retryFn) {
|
||||
const refreshed = await tryRefreshToken();
|
||||
if (refreshed) {
|
||||
const retryResponse = await retryFn();
|
||||
return handleResponse<T>(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<T>(path: string, body?: Record<string, unknown>, options?: { silent?: boolean }): Promise<T> {
|
||||
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<T>(response, options?.silent);
|
||||
const response = await doFetch();
|
||||
return handleResponse<T>(response, options?.silent, doFetch);
|
||||
},
|
||||
|
||||
async get<T>(path: string, options?: { silent?: boolean }): Promise<T> {
|
||||
const response = await fetch(`${API_URL}${path}`, {
|
||||
const doFetch = () => fetch(`${API_URL}${path}`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders(),
|
||||
});
|
||||
return handleResponse<T>(response, options?.silent);
|
||||
const response = await doFetch();
|
||||
return handleResponse<T>(response, options?.silent, doFetch);
|
||||
},
|
||||
|
||||
// Health check — silent, no toasts
|
||||
|
||||
Reference in New Issue
Block a user