Merge branch 'dev-main' into dev-kartik

This commit is contained in:
Kartik Datrika
2026-03-25 11:48:00 +05:30
6 changed files with 83 additions and 72 deletions

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { type ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react";
export type Role = "executive" | "admin" | "cc-agent";
@@ -21,7 +20,7 @@ type AuthContextType = {
loading: boolean;
loginWithUser: (userData: User) => void;
login: () => void;
logout: () => void;
logout: () => Promise<void>;
setRole: (role: Role) => void;
};
@@ -78,7 +77,6 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
localStorage.removeItem("helix_access_token");
localStorage.removeItem("helix_refresh_token");
localStorage.removeItem(STORAGE_KEY);
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoading(false);
}
}, [isAuthenticated, persisted]);
@@ -96,15 +94,28 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
setIsAuthenticated(true);
}, []);
const logout = useCallback(() => {
// Notify sidecar to unlock Redis session + Ozonetel logout
const logout = useCallback(async () => {
// Disconnect SIP before logout
try {
const { disconnectSip } = await import("@/state/sip-manager");
disconnectSip();
} catch {
// SIP disconnect failed or manager not found — ignore
}
// Notify sidecar to unlock Redis session + Ozonetel logout — await before clearing tokens
const token = localStorage.getItem("helix_access_token");
if (token) {
const apiUrl = import.meta.env.VITE_API_URL ?? "http://localhost:4100";
fetch(`${apiUrl}/auth/logout`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
}).catch(() => {});
try {
await fetch(`${apiUrl}/auth/logout`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(5000),
});
} catch (err) {
console.warn("Logout cleanup failed:", err);
}
}
setUser(DEFAULT_USER);
@@ -124,7 +135,19 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
}, []);
return (
<AuthContext.Provider value={{ user, isAdmin, isCCAgent, isAuthenticated, loading, loginWithUser, login, logout, setRole }}>
<AuthContext.Provider
value={{
user,
isAdmin,
isCCAgent,
isAuthenticated,
loading,
loginWithUser,
login,
logout,
setRole,
}}
>
{children}
</AuthContext.Provider>
);

View File

@@ -1,6 +1,7 @@
import { type PropsWithChildren, useCallback, useEffect } from "react";
import { useAtom, useSetAtom } from "jotai";
import { connectSip, disconnectSip, getSipClient, registerSipStateUpdater } from "@/state/sip-manager";
import { apiClient } from "@/lib/api-client";
import { connectSip, disconnectSip, getSipClient, registerSipStateUpdater, setOutboundPending } from "@/state/sip-manager";
import {
sipCallDurationAtom,
sipCallStartTimeAtom,
@@ -87,11 +88,14 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
// No auto-reset — the ActiveCallCard handles post-call flow (disposition → appointment → done)
// and resets to idle via the "Back to Worklist" button
// Cleanup on page unload
// Cleanup on unmount + page unload
useEffect(() => {
const handleUnload = () => disconnectSip();
window.addEventListener("beforeunload", handleUnload);
return () => window.removeEventListener("beforeunload", handleUnload);
return () => {
window.removeEventListener("beforeunload", handleUnload);
disconnectSip();
};
}, []);
return <>{children}</>;
@@ -101,7 +105,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
// eslint-disable-next-line react-refresh/only-export-components
export const useSip = () => {
const [connectionStatus] = useAtom(sipConnectionStatusAtom);
const [callState] = useAtom(sipCallStateAtom);
const [callState, setCallState] = useAtom(sipCallStateAtom);
const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom);
const [callUcid] = useAtom(sipCallUcidAtom);
const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom);
@@ -116,6 +120,27 @@ export const useSip = () => {
[setCallerNumber],
);
// Ozonetel outbound dial — single path for all outbound calls
const dialOutbound = useCallback(
async (phoneNumber: string): Promise<void> => {
setCallState("ringing-out");
setCallerNumber(phoneNumber);
setOutboundPending(true);
const safetyTimeout = setTimeout(() => setOutboundPending(false), 30000);
try {
await apiClient.post("/api/ozonetel/dial", { phoneNumber });
} catch {
clearTimeout(safetyTimeout);
setOutboundPending(false);
setCallState("idle");
setCallerNumber(null);
throw new Error("Dial failed");
}
},
[setCallState, setCallerNumber],
);
const answer = useCallback(() => getSipClient()?.answer(), []);
const reject = useCallback(() => getSipClient()?.reject(), []);
const hangup = useCallback(() => getSipClient()?.hangup(), []);
@@ -153,6 +178,7 @@ export const useSip = () => {
connect: () => connectSip(getSipConfig()),
disconnect: disconnectSip,
makeCall,
dialOutbound,
answer,
reject,
hangup,