Start integrating the onboarding flow with backend 1
This commit is contained in:
215
frontend/src/components/ui/TenantSwitcher.tsx
Normal file
215
frontend/src/components/ui/TenantSwitcher.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTenant } from '../../stores/tenant.store';
|
||||
import { useToast } from '../../hooks/ui/useToast';
|
||||
import { ChevronDown, Building2, Check, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface TenantSwitcherProps {
|
||||
className?: string;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
className = '',
|
||||
showLabel = true,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const {
|
||||
currentTenant,
|
||||
availableTenants,
|
||||
isLoading,
|
||||
error,
|
||||
switchTenant,
|
||||
loadUserTenants,
|
||||
clearError,
|
||||
} = useTenant();
|
||||
|
||||
const { success: showSuccessToast, error: showErrorToast } = useToast();
|
||||
|
||||
// Load tenants on mount
|
||||
useEffect(() => {
|
||||
if (!availableTenants) {
|
||||
loadUserTenants();
|
||||
}
|
||||
}, [availableTenants, loadUserTenants]);
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
!buttonRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Handle tenant switch
|
||||
const handleTenantSwitch = async (tenantId: string) => {
|
||||
if (tenantId === currentTenant?.id) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await switchTenant(tenantId);
|
||||
setIsOpen(false);
|
||||
|
||||
if (success) {
|
||||
const newTenant = availableTenants?.find(t => t.id === tenantId);
|
||||
showSuccessToast(`Switched to ${newTenant?.name}`, {
|
||||
title: 'Tenant Switched'
|
||||
});
|
||||
} else {
|
||||
showErrorToast(error || 'Failed to switch tenant', {
|
||||
title: 'Switch Failed'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle retry loading tenants
|
||||
const handleRetry = () => {
|
||||
clearError();
|
||||
loadUserTenants();
|
||||
};
|
||||
|
||||
// Don't render if no tenants available
|
||||
if (!availableTenants || availableTenants.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't render if only one tenant
|
||||
if (availableTenants.length === 1) {
|
||||
return showLabel ? (
|
||||
<div className={`flex items-center space-x-2 text-text-secondary ${className}`}>
|
||||
<Building2 className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{currentTenant?.name}</span>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
disabled={isLoading}
|
||||
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border-secondary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Switch tenant"
|
||||
>
|
||||
<Building2 className="w-4 h-4 text-text-secondary" />
|
||||
{showLabel && (
|
||||
<span className="hidden sm:block max-w-32 truncate">
|
||||
{currentTenant?.name || 'Select Tenant'}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-text-secondary transition-transform ${
|
||||
isOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="absolute right-0 mt-2 w-72 bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-50"
|
||||
role="listbox"
|
||||
aria-label="Available tenants"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-border-primary">
|
||||
<h3 className="text-sm font-semibold text-text-primary">Switch Organization</h3>
|
||||
<p className="text-xs text-text-secondary">
|
||||
Select the organization you want to work with
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<div className="px-3 py-2 border-b border-border-primary">
|
||||
<div className="flex items-center space-x-2 text-color-error text-xs">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="ml-auto text-color-primary hover:text-color-primary-dark underline"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tenant List */}
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{availableTenants.map((tenant) => (
|
||||
<button
|
||||
key={tenant.id}
|
||||
onClick={() => handleTenantSwitch(tenant.id)}
|
||||
disabled={isLoading}
|
||||
className="w-full px-3 py-3 text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
role="option"
|
||||
aria-selected={tenant.id === currentTenant?.id}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-color-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Building2 className="w-4 h-4 text-color-primary" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-text-primary truncate">
|
||||
{tenant.name}
|
||||
</p>
|
||||
<p className="text-xs text-text-secondary truncate">
|
||||
{tenant.business_type} • {tenant.city}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tenant.id === currentTenant?.id && (
|
||||
<Check className="w-4 h-4 text-color-success flex-shrink-0 ml-2" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-3 py-2 border-t border-border-primary bg-bg-secondary rounded-b-lg">
|
||||
<p className="text-xs text-text-secondary">
|
||||
Need to add a new organization?{' '}
|
||||
<button className="text-color-primary hover:text-color-primary-dark underline">
|
||||
Contact Support
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 bg-bg-primary/50 rounded-lg flex items-center justify-center">
|
||||
<div className="w-4 h-4 border-2 border-color-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user