Optimize client details with opaque tokens
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s

This commit is contained in:
Cauê Faleiros
2026-06-22 11:00:42 -03:00
parent 3c74fa7210
commit 0874238fc6
12 changed files with 548 additions and 43 deletions

View File

@@ -1,10 +1,10 @@
import { useMemo, useState } from 'react';
import { useParams, Link, useOutletContext, useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useParams, Link, useOutletContext } from 'react-router-dom';
import { ArrowLeft, User, Tag, Package, DollarSign, Clock, Phone, ChevronLeft, ChevronRight, ShoppingBag, ReceiptText } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import DateRangePicker from '../components/DateRangePicker';
import type { DateRange, OrderData } from '../types';
import { buildClientDetailsMetrics } from '../analytics/clients';
import type { ClientDetailsAnalytics, DateRange } from '../types';
import { fetchClientDetailsAnalytics } from '../dataService';
type CustomTooltipProps = {
active?: boolean;
@@ -27,34 +27,44 @@ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
};
const ClientDetails = () => {
const { customerKey } = useParams<{ customerKey: string }>();
const decodedCustomerKey = customerKey ? decodeURIComponent(customerKey) : '';
const [searchParams] = useSearchParams();
const requestedName = searchParams.get('name') || '';
const { dateRange, setDateRange, ordersData, isDataLoading } = useOutletContext<{
const { clientToken } = useParams<{ clientToken: string }>();
const decodedClientToken = clientToken ? decodeURIComponent(clientToken) : '';
const { dateRange, setDateRange } = useOutletContext<{
dateRange: DateRange,
setDateRange: (range: DateRange) => void,
ordersData: OrderData[],
isDataLoading: boolean
setDateRange: (range: DateRange) => void
}>();
const [details, setDetails] = useState<ClientDetailsAnalytics | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [ordersPerPage, setOrdersPerPage] = useState(5);
const {
chartData,
groupedOrders,
allTimeOrderCount,
clientName,
clientPhone,
hasClient,
periodAverageTicket,
periodItems,
periodOrderCount,
periodSpent
} = useMemo(() => {
return buildClientDetailsMetrics(ordersData, decodedCustomerKey, dateRange);
}, [dateRange, decodedCustomerKey, ordersData]);
const displayName = requestedName || clientName || decodedCustomerKey.replace(/^name:/, '');
useEffect(() => {
let isMounted = true;
const loadClientDetails = async () => {
if (!decodedClientToken) {
if (isMounted) {
setDetails(null);
setIsLoading(false);
}
return;
}
setIsLoading(true);
const nextDetails = await fetchClientDetailsAnalytics(decodedClientToken, dateRange);
if (isMounted) {
setDetails(nextDetails);
setIsLoading(false);
}
};
void loadClientDetails();
return () => {
isMounted = false;
};
}, [dateRange, decodedClientToken]);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
@@ -69,12 +79,7 @@ const ClientDetails = () => {
setDateRange(range);
};
const totalPages = Math.ceil(groupedOrders.length / ordersPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
const startIndex = (safeCurrentPage - 1) * ordersPerPage;
const paginatedOrders = groupedOrders.slice(startIndex, startIndex + ordersPerPage);
if (!hasClient && isDataLoading) {
if (isLoading) {
return (
<div className="text-center py-12">
<p className="text-zinc-500 dark:text-dark-muted font-medium">Carregando cliente...</p>
@@ -82,7 +87,7 @@ const ClientDetails = () => {
);
}
if (!hasClient) {
if (!details?.hasClient) {
return (
<div className="text-center py-12">
<p className="text-zinc-500 dark:text-dark-muted font-medium">Cliente não encontrado.</p>
@@ -91,6 +96,23 @@ const ClientDetails = () => {
);
}
const {
chartData,
groupedOrders,
allTimeOrderCount,
clientName,
clientPhone,
periodAverageTicket,
periodItems,
periodOrderCount,
periodSpent
} = details;
const displayName = clientName || 'Cliente';
const totalPages = Math.ceil(groupedOrders.length / ordersPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
const startIndex = (safeCurrentPage - 1) * ordersPerPage;
const paginatedOrders = groupedOrders.slice(startIndex, startIndex + ordersPerPage);
return (
<div className="space-y-6">
{/* Header Area */}

View File

@@ -109,6 +109,7 @@ const Clients = () => {
const rfmClient = rfmByCustomerKey.get(client.customerKey);
return {
customerKey: client.customerKey,
clientToken: client.clientToken,
name: client.name,
phone: client.phone,
totalSpent: client.totalSpent,
@@ -315,7 +316,7 @@ const Clients = () => {
</td>
<td className="px-6 py-2.5 text-right">
<Link
to={`/clients/${encodeURIComponent(client.customerKey)}?name=${encodeURIComponent(client.name)}`}
to={`/clients/${encodeURIComponent(client.clientToken)}`}
className="inline-flex items-center text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity cursor-pointer"
>
Ver detalhes

View File

@@ -533,7 +533,7 @@ const Rfm = () => {
return (
<tr key={client.customerKey} className="hover:bg-dark-input/50 transition-colors">
<td className="px-6 py-3">
<Link to={`/clients/${encodeURIComponent(client.customerKey)}?name=${encodeURIComponent(client.name)}`} className="flex items-center gap-3 hover:text-brand-primary transition-colors">
<Link to={`/clients/${encodeURIComponent(client.clientToken)}`} className="flex items-center gap-3 hover:text-brand-primary transition-colors">
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-dark-input text-dark-muted">
<Users className="h-4 w-4" />
</span>