Compare commits

...

2 Commits

Author SHA1 Message Date
Cauê Faleiros
6fb86b4806 feat: implement real user profile and authentication state
All checks were successful
Build and Deploy / build-and-push (push) Successful in 2m3s
2026-02-26 10:36:59 -03:00
Cauê Faleiros
dda606ef9b feat: implement real user profile viewing and auth state 2026-02-26 10:18:27 -03:00
3 changed files with 72 additions and 34 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import { LayoutDashboard, Users, UserCircle, Bell, Search, Menu, X, LogOut, Hexagon, Settings, Building2 } from 'lucide-react'; import { LayoutDashboard, Users, UserCircle, Bell, Search, Menu, X, LogOut, Hexagon, Settings, Building2 } from 'lucide-react';
import { USERS } from '../constants'; import { getAttendances, getUsers, getUserById } from '../services/dataService';
import { User } from '../types'; import { User } from '../types';
const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: any, label: string, collapsed: boolean }) => ( const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: any, label: string, collapsed: boolean }) => (
@@ -24,20 +24,33 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [currentUser, setCurrentUser] = useState<User>(USERS[1]); // Default to standard user fallback const [currentUser, setCurrentUser] = useState<User | null>(null);
useEffect(() => { useEffect(() => {
const storedUserId = localStorage.getItem('ctms_user_id'); const fetchCurrentUser = async () => {
if (storedUserId) { const storedUserId = localStorage.getItem('ctms_user_id');
const user = USERS.find(u => u.id === storedUserId); if (!storedUserId) {
if (user) { navigate('/login');
setCurrentUser(user); return;
} }
}
}, []); try {
const user = await getUserById(storedUserId);
if (user) {
setCurrentUser(user);
} else {
navigate('/login');
}
} catch (err) {
console.error("Layout fetch failed:", err);
}
};
fetchCurrentUser();
}, [navigate]);
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('ctms_user_id'); localStorage.removeItem('ctms_user_id');
localStorage.removeItem('ctms_tenant_id');
navigate('/login'); navigate('/login');
}; };
@@ -52,6 +65,8 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
return 'CTMS'; return 'CTMS';
}; };
if (!currentUser) return null;
const isSuperAdmin = currentUser.role === 'super_admin'; const isSuperAdmin = currentUser.role === 'super_admin';
return ( return (

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Hexagon, Lock, Mail, ArrowRight, Loader2, Info } from 'lucide-react'; import { Hexagon, Lock, Mail, ArrowRight, Loader2, Info } from 'lucide-react';
import { USERS } from '../constants'; import { getUsers } from '../services/dataService';
export const Login: React.FC = () => { export const Login: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -10,22 +10,22 @@ export const Login: React.FC = () => {
const [password, setPassword] = useState('password'); const [password, setPassword] = useState('password');
const [error, setError] = useState(''); const [error, setError] = useState('');
const handleLogin = (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
// Simulate API call and validation try {
setTimeout(() => { // Fetch all users to find match (simplified auth for demo)
const user = USERS.find(u => u.email.toLowerCase() === email.toLowerCase()); const users = await getUsers('all');
const user = users.find(u => u.email.toLowerCase() === email.toLowerCase());
if (user) { if (user) {
// Mock Login: Save to local storage
localStorage.setItem('ctms_user_id', user.id); localStorage.setItem('ctms_user_id', user.id);
localStorage.setItem('ctms_tenant_id', user.tenant_id || '');
setIsLoading(false); setIsLoading(false);
// Redirect based on role
if (user.role === 'super_admin') { if (user.role === 'super_admin') {
navigate('/super-admin'); navigate('/super-admin');
} else { } else {
@@ -33,9 +33,13 @@ export const Login: React.FC = () => {
} }
} else { } else {
setIsLoading(false); setIsLoading(false);
setError('Usuário não encontrado. Tente lidya@fasto.com ou root@system.com'); setError('Usuário não encontrado.');
} }
}, 1000); } catch (err) {
console.error("Login error:", err);
setIsLoading(false);
setError('Erro ao conectar ao servidor.');
}
}; };
const fillCredentials = (type: 'admin' | 'super') => { const fillCredentials = (type: 'admin' | 'super') => {

View File

@@ -1,24 +1,41 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Camera, Save, Mail, User as UserIcon, Building, Shield, Loader2, CheckCircle2 } from 'lucide-react'; import { Camera, Save, Mail, User as UserIcon, Building, Shield, Loader2, CheckCircle2 } from 'lucide-react';
import { USERS } from '../constants'; import { getUserById, getTenants } from '../services/dataService';
import { User } from '../types'; import { User, Tenant } from '../types';
export const UserProfile: React.FC = () => { export const UserProfile: React.FC = () => {
// Simulating logged-in user state
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [tenant, setTenant] = useState<Tenant | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false); const [isSuccess, setIsSuccess] = useState(false);
// Form State
const [name, setName] = useState(''); const [name, setName] = useState('');
const [bio, setBio] = useState(''); const [bio, setBio] = useState('');
useEffect(() => { useEffect(() => {
// Simulate fetching user data const fetchUserAndTenant = async () => {
const currentUser = USERS[0]; const storedUserId = localStorage.getItem('ctms_user_id');
setUser(currentUser); if (storedUserId) {
setName(currentUser.name); try {
setBio(currentUser.bio || ''); const fetchedUser = await getUserById(storedUserId);
if (fetchedUser) {
setUser(fetchedUser);
setName(fetchedUser.name);
setBio(fetchedUser.bio || '');
// Fetch tenant info
const tenants = await getTenants();
const userTenant = tenants.find(t => t.id === fetchedUser.tenant_id);
if (userTenant) {
setTenant(userTenant);
}
}
} catch (err) {
console.error("Error fetching profile data:", err);
}
}
};
fetchUserAndTenant();
}, []); }, []);
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
@@ -50,7 +67,7 @@ export const UserProfile: React.FC = () => {
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 flex flex-col items-center text-center"> <div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 flex flex-col items-center text-center">
<div className="relative group cursor-pointer"> <div className="relative group cursor-pointer">
<div className="w-32 h-32 rounded-full overflow-hidden border-4 border-slate-50 shadow-sm"> <div className="w-32 h-32 rounded-full overflow-hidden border-4 border-slate-50 shadow-sm">
<img src={user.avatar_url} alt={user.name} className="w-full h-full object-cover" /> <img src={user.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random`} alt={user.name} className="w-full h-full object-cover" />
</div> </div>
<div className="absolute inset-0 bg-black/40 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200"> <div className="absolute inset-0 bg-black/40 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<Camera className="text-white" size={28} /> <Camera className="text-white" size={28} />
@@ -67,21 +84,23 @@ export const UserProfile: React.FC = () => {
<span className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-semibold capitalize border border-purple-200"> <span className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-semibold capitalize border border-purple-200">
{user.role} {user.role}
</span> </span>
<span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-xs font-semibold border border-blue-200"> {user.team_id && (
{user.team_id === 'sales_1' ? 'Vendas Alpha' : 'Vendas Beta'} <span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-xs font-semibold border border-blue-200">
</span> {user.team_id === 'sales_1' ? 'Vendas Alpha' : user.team_id === 'sales_2' ? 'Vendas Beta' : user.team_id}
</span>
)}
</div> </div>
</div> </div>
<div className="bg-slate-100 rounded-xl p-4 border border-slate-200"> <div className="bg-slate-100 rounded-xl p-4 border border-slate-200">
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Status da Conta</h3> <h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Status da Conta</h3>
<div className="flex items-center gap-3 text-sm text-slate-600 mb-2"> <div className="flex items-center gap-3 text-sm text-slate-600 mb-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div> <div className={`w-2 h-2 rounded-full ${user.status === 'active' ? 'bg-green-500' : 'bg-slate-400'}`}></div>
Ativo {user.status === 'active' ? 'Ativo' : 'Inativo'}
</div> </div>
<div className="flex items-center gap-3 text-sm text-slate-600"> <div className="flex items-center gap-3 text-sm text-slate-600">
<Building size={14} /> <Building size={14} />
Fasto Corp (Organização) {tenant?.name || 'Organização'}
</div> </div>
</div> </div>
</div> </div>