Optimize client details with opaque tokens
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user