feat: complete UI/UX refinement, email flow updates, and deep black theme
All checks were successful
Build and Deploy / build-and-push (push) Successful in 2m18s

- Updated all email templates to a clean light theme and changed button text to 'Finalizar Cadastro'.

- Enforced a strict 15-minute expiration on all auth/reset tokens.

- Created SetupAccount flow distinct from ResetPassword to capture user name during admin init.

- Refined dark mode to a premium True Black (Onyx) palette using Zinc.

- Fixed Dashboard KPI visibility and true period-over-period trend logic.

- Enhanced TeamManagement with global tenant filtering for Super Admins.

- Implemented secure User URL routing via slugs instead of raw UUIDs.

- Enforced strict Agent-level RBAC for viewing attendances.
This commit is contained in:
Cauê Faleiros
2026-03-05 15:33:03 -03:00
parent d5b57835a7
commit c4bd4d58a1
14 changed files with 369 additions and 70 deletions

View File

@@ -25,6 +25,7 @@ interface SellerStats {
export const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<Attendance[]>([]);
const [prevData, setPrevData] = useState<Attendance[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [teams, setTeams] = useState<any[]>([]);
const [currentUser, setCurrentUser] = useState<User | null>(null);
@@ -48,16 +49,24 @@ export const Dashboard: React.FC = () => {
const storedUserId = localStorage.getItem('ctms_user_id');
if (!tenantId) return;
// Calculate previous date range for accurate period-over-period trend
const duration = filters.dateRange.end.getTime() - filters.dateRange.start.getTime();
const prevStart = new Date(filters.dateRange.start.getTime() - duration);
const prevEnd = new Date(filters.dateRange.start.getTime());
const prevFilters = { ...filters, dateRange: { start: prevStart, end: prevEnd } };
// Fetch users, attendances, teams and current user in parallel
const [fetchedUsers, fetchedData, fetchedTeams, me] = await Promise.all([
const [fetchedUsers, fetchedData, prevFetchedData, fetchedTeams, me] = await Promise.all([
getUsers(tenantId),
getAttendances(tenantId, filters),
getAttendances(tenantId, prevFilters),
getTeams(tenantId),
storedUserId ? getUserById(storedUserId) : null
]);
setUsers(fetchedUsers);
setData(fetchedData);
setPrevData(prevFetchedData);
setTeams(fetchedTeams);
if (me) setCurrentUser(me);
} catch (error) {
@@ -74,6 +83,48 @@ export const Dashboard: React.FC = () => {
const avgScore = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.score, 0) / data.length).toFixed(1) : "0";
const avgResponseTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.first_response_time_min, 0) / data.length).toFixed(0) : "0";
const avgHandleTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.handling_time_min, 0) / data.length).toFixed(0) : "0";
// --- Dynamic Trends Calculation ---
const trends = useMemo(() => {
if (data.length === 0 && prevData.length === 0) {
return { leads: null, score: null, resp: null };
}
const calcAvg = (arr: Attendance[], key: keyof Attendance) =>
arr.length ? arr.reduce((acc, curr) => acc + (curr[key] as number), 0) / arr.length : 0;
// Leads Trend (%)
const recentLeads = data.length;
const prevLeads = prevData.length;
let leadsTrend = 0;
if (prevLeads > 0) {
leadsTrend = ((recentLeads - prevLeads) / prevLeads) * 100;
} else if (recentLeads > 0) {
leadsTrend = 100;
}
// Score Trend (Absolute point difference)
const recentScore = calcAvg(data, 'score');
const prevScore = calcAvg(prevData, 'score');
let scoreTrend = 0;
if (prevData.length > 0) {
scoreTrend = recentScore - prevScore;
} else {
scoreTrend = recentScore;
}
// Response Time Trend (%)
const recentResp = calcAvg(data, 'first_response_time_min');
const prevResp = calcAvg(prevData, 'first_response_time_min');
let respTrend = 0;
if (prevResp > 0) {
respTrend = ((recentResp - prevResp) / prevResp) * 100;
} else if (recentResp > 0) {
respTrend = 100; // Time increased from 0, which is worse
}
return { leads: leadsTrend, score: scoreTrend, resp: respTrend };
}, [data, prevData]);
// --- Chart Data: Funnel ---
const funnelData = useMemo(() => {
@@ -245,25 +296,25 @@ export const Dashboard: React.FC = () => {
<KPICard
title="Total de Leads"
value={totalLeads}
trend="up"
trendValue="12%"
trend={trends.leads > 0 ? 'up' : trends.leads < 0 ? 'down' : 'neutral'}
trendValue={`${Math.abs(trends.leads).toFixed(1)}%`}
icon={Users}
colorClass="text-yellow-600"
colorClass="text-brand-yellow"
/>
<KPICard
title="Nota Média Qualidade"
value={avgScore}
subValue="/ 100"
trend={Number(avgScore) > 75 ? 'up' : 'down'}
trendValue="2.1"
trend={trends.score > 0 ? 'up' : trends.score < 0 ? 'down' : 'neutral'}
trendValue={Math.abs(trends.score).toFixed(1)}
icon={TrendingUp}
colorClass="text-zinc-600"
colorClass="text-zinc-500"
/>
<KPICard
title="Média 1ª Resposta"
value={`${avgResponseTime}m`}
trend="down"
trendValue="bom"
trend={trends.resp < 0 ? 'up' : trends.resp > 0 ? 'down' : 'neutral'} // Faster response is better (up)
trendValue={`${Math.abs(trends.resp).toFixed(1)}%`}
icon={Clock}
colorClass="text-orange-500"
/>