// Copyright: © 2026 TWWIM UG. All rights reserved. (www.twwim.com) /** * OperatorAvailabilityToggle — pill-shaped switch in the Comm Center header. * * - green dot + "I'm available" when isAvailable=true * - grey dot + "I'm away" when isAvailable=false * - neutral pill ("Not an operator") when the API returns 403 (caller is * ADMIN / MANAGER / WORKER — they have no presence state to flip) * - spinner while a mutation is in flight * * Click flips the value via PATCH /operator/availability. Persistence lives * in customer_data.company_membership.is_available; presence is the only * thing this toggle moves — routing logic (AI vs operator) is unchanged. */ import { Loader2 } from 'lucide-react'; import { useOperatorAvailability, useSetOperatorAvailability, } from '../hooks/useOperatorAvailability'; interface ApiLikeError { status?: number; response?: { status?: number }; } function statusOf(err: unknown): number | undefined { const candidate = err as ApiLikeError | null; return candidate?.status ?? candidate?.response?.status; } export function OperatorAvailabilityToggle() { const query = useOperatorAvailability(); const mutation = useSetOperatorAvailability(); // 403 → caller is not an OPERATOR; render a neutral disabled pill so the UI // doesn't lie about a non-existent presence flag. if (statusOf(query.error) === 403) { return ( Not an operator ); } if (query.isLoading) { return ( Loading… ); } if (query.isError || !query.data) { return ( Availability unavailable ); } const { isAvailable } = query.data; const onToggle = () => { if (mutation.isPending) return; mutation.mutate({ isAvailable: !isAvailable }); }; const palette = isAvailable ? 'bg-emerald-50 text-emerald-700 ring-emerald-200 hover:bg-emerald-100' : 'bg-gray-50 text-gray-600 ring-gray-200 hover:bg-gray-100'; const dot = isAvailable ? 'bg-emerald-500 animate-pulse' : 'bg-gray-400'; return ( ); }