Files
fasto/pages/UserDetail.tsx
Cauê Faleiros 9f047ece86
All checks were successful
Build and Deploy / build-and-push (push) Successful in 2m12s
feat: implement real user login state and fix profile view
2026-02-25 11:59:53 -03:00

213 lines
10 KiB
TypeScript

import React, { useEffect, useState, useMemo } from 'react';
import { useParams, Link } from 'react-router-dom';
import { getAttendances, getUserById } from '../services/dataService';
import { CURRENT_TENANT_ID } from '../constants';
import { Attendance, User, FunnelStage } from '../types';
import { ArrowLeft, Mail, Phone, Clock, MessageSquare, ChevronLeft, ChevronRight, Eye } from 'lucide-react';
const ITEMS_PER_PAGE = 10;
export const UserDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [user, setUser] = useState<User | undefined>();
const [attendances, setAttendances] = useState<Attendance[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
if (id) {
setLoading(true);
const loadData = async () => {
try {
const tenantId = localStorage.getItem('ctms_tenant_id') || CURRENT_TENANT_ID;
const u = await getUserById(id);
setUser(u);
if (u) {
const data = await getAttendances(tenantId, {
userId: id,
dateRange: { start: new Date(0), end: new Date() } // All time
});
setAttendances(data);
}
} catch (error) {
console.error("Error loading user details", error);
} finally {
setLoading(false);
}
};
loadData();
}
}, [id]);
const totalPages = Math.ceil(attendances.length / ITEMS_PER_PAGE);
const currentData = useMemo(() => {
const start = (currentPage - 1) * ITEMS_PER_PAGE;
return attendances.slice(start, start + ITEMS_PER_PAGE);
}, [attendances, currentPage]);
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
setCurrentPage(newPage);
}
};
const getStageBadgeColor = (stage: FunnelStage) => {
switch (stage) {
case FunnelStage.WON: return 'bg-green-100 text-green-700 border-green-200';
case FunnelStage.LOST: return 'bg-red-100 text-red-700 border-red-200';
case FunnelStage.NEGOTIATION: return 'bg-blue-100 text-blue-700 border-blue-200';
case FunnelStage.IDENTIFICATION: return 'bg-yellow-100 text-yellow-700 border-yellow-200';
default: return 'bg-slate-100 text-slate-700 border-slate-200';
}
};
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-green-600 bg-green-50';
if (score >= 60) return 'text-yellow-600 bg-yellow-50';
return 'text-red-600 bg-red-50';
};
if (!loading && !user) return <div className="p-8 text-slate-500">Usuário não encontrado</div>;
return (
<div className="space-y-8 max-w-7xl mx-auto">
{/* Header Section */}
<div className="flex flex-col md:flex-row gap-6 items-start md:items-center justify-between">
<div className="flex items-center gap-4">
<Link to="/" className="p-2.5 bg-white border border-slate-200 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-900 transition-colors shadow-sm">
<ArrowLeft size={18} />
</Link>
{user && (
<div className="flex items-center gap-4">
<img src={user.avatar_url} alt={user.name} className="w-16 h-16 rounded-full border-2 border-white shadow-md object-cover" />
<div>
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">{user.name}</h1>
<div className="flex items-center gap-3 text-sm text-slate-500 mt-1">
<span className="flex items-center gap-1.5"><Mail size={14} /> {user.email}</span>
<span className="bg-slate-100 text-slate-600 px-2 py-0.5 rounded text-xs font-semibold uppercase">{user.role}</span>
</div>
</div>
</div>
)}
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<div className="text-sm font-medium text-slate-500 mb-2">Total de Interações</div>
<div className="text-3xl font-bold text-slate-900">{attendances.length}</div>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<div className="text-sm font-medium text-slate-500 mb-2">Taxa de Conversão</div>
<div className="text-3xl font-bold text-blue-600">
{attendances.length ? ((attendances.filter(a => a.converted).length / attendances.length) * 100).toFixed(1) : 0}%
</div>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<div className="text-sm font-medium text-slate-500 mb-2">Nota Média</div>
<div className="text-3xl font-bold text-yellow-500">
{attendances.length ? (attendances.reduce((acc, c) => acc + c.score, 0) / attendances.length).toFixed(1) : 0}
</div>
</div>
</div>
{/* Attendance Table */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex flex-col">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
<h2 className="font-semibold text-slate-900">Histórico de Atendimentos</h2>
<span className="text-xs text-slate-500 font-medium bg-slate-200/50 px-2 py-1 rounded">Página {currentPage} de {totalPages || 1}</span>
</div>
{loading ? (
<div className="p-12 text-center text-slate-400">Carregando registros...</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-100 text-xs uppercase text-slate-500 bg-slate-50/30">
<th className="px-6 py-4 font-medium">Data / Hora</th>
<th className="px-6 py-4 font-medium w-1/3">Resumo</th>
<th className="px-6 py-4 font-medium text-center">Etapa</th>
<th className="px-6 py-4 font-medium text-center">Nota</th>
<th className="px-6 py-4 font-medium text-center">T. Resp.</th>
<th className="px-6 py-4 font-medium text-right">Ação</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 text-sm">
{currentData.map(att => (
<tr key={att.id} className="hover:bg-slate-50/80 transition-colors group">
<td className="px-6 py-4 text-slate-600 whitespace-nowrap">
<div className="font-medium text-slate-900">{new Date(att.created_at).toLocaleDateString()}</div>
<div className="text-xs text-slate-400">{new Date(att.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div>
</td>
<td className="px-6 py-4">
<div className="flex flex-col">
<span className="text-slate-800 line-clamp-1 font-medium mb-1">{att.summary}</span>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span className="flex items-center gap-1"><MessageSquare size={10} /> {att.origin}</span>
</div>
</div>
</td>
<td className="px-6 py-4 text-center">
<span className={`inline-flex px-2.5 py-0.5 rounded-full text-xs font-semibold border ${getStageBadgeColor(att.funnel_stage)}`}>
{att.funnel_stage}
</span>
</td>
<td className="px-6 py-4 text-center">
<span className={`inline-flex items-center justify-center w-8 h-8 rounded-lg font-bold text-sm ${getScoreColor(att.score)}`}>
{att.score}
</span>
</td>
<td className="px-6 py-4 text-center text-slate-600">
<div className="flex items-center justify-center gap-1">
<Clock size={14} className="text-slate-400" />
{att.first_response_time_min}m
</div>
</td>
<td className="px-6 py-4 text-right">
<Link
to={`/attendances/${att.id}`}
className="inline-flex items-center justify-center p-2 rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 transition-all"
title="Ver Detalhes"
>
<Eye size={18} />
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Pagination Footer */}
{totalPages > 1 && (
<div className="px-6 py-4 border-t border-slate-100 flex items-center justify-between bg-slate-50/30">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="flex items-center gap-1 px-3 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm"
>
<ChevronLeft size={16} /> Anterior
</button>
<div className="text-sm text-slate-500">
Mostrando <span className="font-medium text-slate-900">{((currentPage - 1) * ITEMS_PER_PAGE) + 1}</span> a <span className="font-medium text-slate-900">{Math.min(currentPage * ITEMS_PER_PAGE, attendances.length)}</span> de {attendances.length}
</div>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="flex items-center gap-1 px-3 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm"
>
Próximo <ChevronRight size={16} />
</button>
</div>
)}
</div>
</div>
);
};