'use client' import { useState, useRef, useEffect, useCallback } from 'react' import { createPortal } from 'react-dom' import Link from 'next/link' import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { useCompany } from '@/contexts/CompanyContext' import { switchCompany } from '@/lib/company/actions' import { useToast } from '@/components/ui/use-toast' import { Check, ChevronsUpDown, Plus, Loader2 } from 'lucide-react' export default function CompanySwitcher() { const { company, companies, isSandbox } = useCompany() const t = useTranslations('company_switcher') const { toast } = useToast() const [open, setOpen] = useState(false) const [isPending, setIsPending] = useState(false) const triggerRef = useRef(null) const dropdownRef = useRef(null) const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0 }) const updatePosition = useCallback(() => { if (!triggerRef.current || !dropdownRef.current) return const triggerRect = triggerRef.current.getBoundingClientRect() const dropdownRect = dropdownRef.current.getBoundingClientRect() const margin = 8 let top = triggerRect.bottom + 4 let left = triggerRect.left // Clamp right edge to viewport if (left + dropdownRect.width > window.innerWidth - margin) { left = Math.max(margin, window.innerWidth - dropdownRect.width - margin) } // If dropdown would go below viewport, show above trigger if (top + dropdownRect.height > window.innerHeight - margin) { top = Math.max(margin, triggerRect.top - dropdownRect.height - 4) } setDropdownPos({ top, left }) }, []) // Update position when opening (run twice: once to render, once to measure) useEffect(() => { if (!open) return const raf = requestAnimationFrame(() => updatePosition()) return () => cancelAnimationFrame(raf) }, [open, updatePosition]) // Close on outside click useEffect(() => { if (!open) return function handleClick(e: MouseEvent) { const target = e.target as Node if ( (!triggerRef.current || !triggerRef.current.contains(target)) && (!dropdownRef.current || !dropdownRef.current.contains(target)) ) { setOpen(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [open]) // Close on Escape useEffect(() => { if (!open) return function handleKey(e: KeyboardEvent) { if (e.key === 'Escape') setOpen(false) } document.addEventListener('keydown', handleKey) return () => document.removeEventListener('keydown', handleKey) }, [open]) const handleSwitch = async (companyId: string) => { if (company && companyId === company.id) { setOpen(false) return } setIsPending(true) const result = await switchCompany(companyId) if (result.error) { setIsPending(false) toast({ title: t(result.error === 'not_member' ? 'error_no_access' : 'error_switch_failed'), variant: 'destructive', }) return } // Notify every other open tab of the same user so they hard-reload // onto the new company. BroadcastChannel is best-effort — if the // browser doesn't support it (very old) we still hard-reload // ourselves, and other tabs will self-correct via the visibilitychange // / pageshow listeners in CompanyTabSync on their next focus event. if (typeof BroadcastChannel !== 'undefined') { try { const channel = new BroadcastChannel('gnubok-company-switch') channel.postMessage({ companyId }) channel.close() } catch { // Ignore — hard reload still happens below } } // Hard navigation — tears down React state, router cache, in-flight // fetches, blob URLs, etc. This is the whole point: nothing from the // previous company can survive the switch. window.location.assign('/') } // Always allow opening the dropdown (to show "Lägg till företag") const hasMultiple = companies.length > 1 // No companies yet — show a direct "Lägg till företag" link instead of // the switcher so the user can still create one. Hidden in sandbox mode. if (!company && companies.length === 0) { if (isSandbox) return null return ( {t('add_company')} ) } return (
{open && createPortal(
{companies.length > 0 && ( <> {hasMultiple && (

{t('company_label')}

)}
{companies.map(({ company: c, role }) => ( ))}
)} {!isSandbox && (
0 && 'border-t border-border/40 mt-1 pt-1', 'px-1')}> setOpen(false)} className="flex items-center gap-2 px-2.5 py-2 text-[13px] text-muted-foreground hover:text-foreground hover:bg-muted/40 rounded-md transition-colors md:whitespace-nowrap" > {t('add_company')}
)}
, document.body )}
) }