/** * Whether the CURRENT user may use the staff live-chat — the single source of * truth for the client-side gate shared by , and the * connect plugin (04.chat-socket.client.ts). * * The server (server/utils/chatAccess.ts) is authoritative; this only controls * whether the UI renders and whether we open a socket. Requires ALL of: * - chat module enabled (config.public.chatEnabled) * - a logged-in user (logship_user_id cookie) * - a non-limited role (getMenuType() !== 'c') * - the user's IsChatEnabled (custom ad_user column, carried in the * logship_user cookie via trimUser) * * The cookie copy of IsChatEnabled is only the OPTIMISTIC seed: resolveChatEligible() * (called once by the connect plugin) reconciles it against the server, which reads * the DB. So a colleague who gets IsChatEnabled set AFTER their last login becomes * eligible WITHOUT re-logging-in. Both casings accepted (iDempiere OData casing varies). */ export const useChatEligible = () => { const config = useRuntimeConfig() const { getMenuType } = useRoleMenu() const userId = useCookie('logship_user_id') const user = useCookie('logship_user') const resolved = useState('chatEligibleResolved', () => null) return computed(() => { if (config.public.chatEnabled !== true) return false if (!userId.value) return false if (getMenuType() === 'c') return false // Server verdict wins once known; until then trust the optimistic cookie flag // (no icon flicker for already-enabled users; the server reconciles the rest). if (resolved.value !== null) return resolved.value const u: any = user.value return u?.IsChatEnabled === true || u?.isChatEnabled === true }) } /** * Reconcile eligibility against the authoritative server gate (DB IsChatEnabled), * setting the shared `chatEligibleResolved` state consumed by useChatEligible(). * Client-only; called once by plugins/04.chat-socket.client.ts before connecting. * Returns the effective eligibility (server verdict, or the cookie flag on a * network error so we still connect optimistically when the server is briefly down). */ export const resolveChatEligible = async (): Promise => { const config = useRuntimeConfig() const { getMenuType } = useRoleMenu() const userId = useCookie('logship_user_id') const user = useCookie('logship_user') const resolved = useState('chatEligibleResolved', () => null) const cookieFlag = () => { const u: any = user.value return u?.IsChatEnabled === true || u?.isChatEnabled === true } // "Chat disabled" is a static negative — safe to cache as the resolved verdict. if (config.public.chatEnabled !== true) { resolved.value = false return false } // "Not logged in" / "limited role" are LOGIN-dependent: they flip after sign-in. // Do NOT persist them as `resolved` — a cached `false` from the logged-out app boot // (the plugin runs once, on the signin page) would survive an SPA login and keep // the chat icon hidden until a full page reload. Leaving `resolved` null lets the // badge fall back to the optimistic cookie flag the moment it mounts post-login, // and the plugin's post-login re-bootstrap then confirms against the server. if (!userId.value || getMenuType() === 'c') { return false } try { const res: any = await $fetch('/api/chat/eligible', { headers: useRequestHeaders(['cookie']) }) resolved.value = res?.eligible === true return resolved.value } catch { return cookieFlag() // network error → optimistic cookie value; resolved stays null } }