Organizacao ativa
This commit is contained in:
parent
211cc1ba6b
commit
fd3f93afa3
2 changed files with 121 additions and 5 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Layout, ListTodo, PlusCircle, Columns, LogOut, Settings, Building2, Clock } from 'lucide-react'
|
||||
import { Layout, ListTodo, PlusCircle, Columns, LogOut, Settings, Building2, Clock, ChevronDown, Shield } from 'lucide-react'
|
||||
import { NavLink, Outlet, Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { OnboardingModal } from './OnboardingModal'
|
||||
|
||||
import { cn } from '../lib/utils'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const navLinkClassName = ({ isActive }: { isActive: boolean }) =>
|
||||
cn(
|
||||
|
|
@ -15,8 +16,9 @@ const navLinkClassName = ({ isActive }: { isActive: boolean }) =>
|
|||
)
|
||||
|
||||
export function AppShell() {
|
||||
const { user, isAuthenticated, isLoading, logout } = useAuth()
|
||||
const { user, isAuthenticated, isLoading, logout, activeOrg, setActiveOrg } = useAuth()
|
||||
const [showOrgModal, setShowOrgModal] = useState(false)
|
||||
const [showOrgSwitcher, setShowOrgSwitcher] = useState(false)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -50,6 +52,92 @@ export function AppShell() {
|
|||
</div>
|
||||
</NavLink>
|
||||
|
||||
<div className="h-8 w-px bg-border-soft hidden lg:block" />
|
||||
|
||||
{/* Org Switcher */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowOrgSwitcher(!showOrgSwitcher)}
|
||||
className="flex items-center gap-2 rounded-xl border border-border-soft bg-surface-muted/50 px-4 py-2 text-sm font-bold transition-all hover:bg-surface-muted group"
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg bg-brand/10 text-brand group-hover:bg-brand group-hover:text-white transition-all">
|
||||
<Building2 size={14} />
|
||||
</div>
|
||||
<div className="text-left hidden sm:block">
|
||||
<span className="block text-xs text-text-main leading-tight truncate max-w-[120px]">
|
||||
{activeOrg?.name || 'Selecionar Org'}
|
||||
</span>
|
||||
<span className="block text-[10px] text-text-muted font-black uppercase tracking-tighter leading-none">
|
||||
{activeOrg?.roles[0] || 'Visitante'}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown size={14} className={cn("text-text-muted transition-transform", showOrgSwitcher && "rotate-180")} />
|
||||
</button>
|
||||
|
||||
{showOrgSwitcher && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setShowOrgSwitcher(false)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="absolute left-0 mt-2 z-50 w-64 rounded-2xl border border-border-soft bg-surface p-2 shadow-xl shadow-brand/10"
|
||||
>
|
||||
<div className="px-3 py-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
|
||||
Suas Organizações
|
||||
</div>
|
||||
<div className="grid gap-1 mt-1">
|
||||
{user?.organizations.map((org) => (
|
||||
<button
|
||||
key={org.id}
|
||||
onClick={() => {
|
||||
setActiveOrg(org.id);
|
||||
setShowOrgSwitcher(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-xl px-3 py-2.5 transition-all text-left",
|
||||
org.id === activeOrg?.id
|
||||
? "bg-brand/10 text-brand"
|
||||
: "hover:bg-surface-muted text-text-muted hover:text-text-main"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-lg font-bold text-xs shadow-sm",
|
||||
org.id === activeOrg?.id ? "bg-brand text-white" : "bg-white border border-border-soft text-text-muted"
|
||||
)}>
|
||||
{org.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-sm font-bold leading-none">{org.name}</span>
|
||||
<span className="text-[10px] font-medium opacity-70">
|
||||
{org.roles.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{org.id === activeOrg?.id && <div className="h-1.5 w-1.5 rounded-full bg-brand" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 border-t border-border-soft p-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowOrgModal(true);
|
||||
setShowOrgSwitcher(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-xs font-bold text-brand hover:bg-brand/5 transition-all"
|
||||
>
|
||||
<PlusCircle size={14} />
|
||||
Gerenciar Organizações
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="hidden items-center gap-1 md:flex">
|
||||
<NavLink to="/" end className={navLinkClassName}>
|
||||
<ListTodo size={18} />
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ interface AuthContextType {
|
|||
user: LoginResponse | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
activeOrg: LoginResponse["organizations"][number] | null;
|
||||
login: (email: string, password: string) => Promise<LoginResponse>;
|
||||
logout: () => void;
|
||||
refreshUser: () => Promise<void>;
|
||||
setActiveOrg: (id: number) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
|
@ -22,6 +24,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
children,
|
||||
}) => {
|
||||
const [user, setUser] = useState<LoginResponse | null>(null);
|
||||
const [activeOrgId, setActiveOrgId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
|
|
@ -72,6 +75,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
const userData: LoginResponse = JSON.parse(storedUser);
|
||||
if (checkTokenExpiration(userData)) {
|
||||
setUser(userData);
|
||||
|
||||
const storedOrgId = localStorage.getItem("activeOrgId");
|
||||
if (storedOrgId) {
|
||||
setActiveOrgId(Number(storedOrgId));
|
||||
} else if (userData.organizations.length > 0) {
|
||||
const firstOrgId = userData.organizations[0].id;
|
||||
setActiveOrgId(firstOrgId);
|
||||
localStorage.setItem("activeOrgId", firstOrgId.toString());
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem("user");
|
||||
|
|
@ -101,30 +113,46 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
const refreshUser = async () => {
|
||||
try {
|
||||
const response = await authService.me();
|
||||
// We keep the old tokens since /me might not return new ones
|
||||
// or we just update the profile part.
|
||||
// But actually, let's merge the updated profile data into the stored user.
|
||||
const storedUser = localStorage.getItem("user");
|
||||
if (storedUser) {
|
||||
const userData = JSON.parse(storedUser);
|
||||
const updatedUser = { ...userData, ...response };
|
||||
localStorage.setItem("user", JSON.stringify(updatedUser));
|
||||
setUser(updatedUser);
|
||||
|
||||
// Update active org if not set or if current active org is gone
|
||||
if (!activeOrgId && updatedUser.organizations.length > 0) {
|
||||
const firstOrgId = updatedUser.organizations[0].id;
|
||||
setActiveOrgId(firstOrgId);
|
||||
localStorage.setItem("activeOrgId", firstOrgId.toString());
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh user profile", error);
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveOrg = (id: number) => {
|
||||
setActiveOrgId(id);
|
||||
localStorage.setItem("activeOrgId", id.toString());
|
||||
};
|
||||
|
||||
const activeOrg = React.useMemo(() =>
|
||||
user?.organizations.find(org => org.id === activeOrgId) || null,
|
||||
[user, activeOrgId]
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
isLoading,
|
||||
activeOrg,
|
||||
login,
|
||||
logout,
|
||||
refreshUser,
|
||||
setActiveOrg,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
Loading…
Reference in a new issue