The useReports hook provides computed analytics and key performance indicators (KPIs) from sales data, including revenue metrics, top products, and recent transactions.
Import
import { useReports } from '@hooks/useReports';
Basic Usage
function ReportsPage() {
const { kpis, topProductos, ventasRecientes } = useReports();
return (
<div>
{/* KPI Dashboard */}
<div className="kpi-grid">
<div>Total Sales: {kpis.totalVentas}</div>
<div>Revenue: S/ {kpis.ingresosTotales.toFixed(2)}</div>
<div>Avg Ticket: S/ {kpis.ticketPromedio.toFixed(2)}</div>
<div>Unique Customers: {kpis.clientesUnicos}</div>
</div>
{/* Top Products */}
<h2>Top Products by Revenue</h2>
<ul>
{topProductos.map(product => (
<li key={product.id}>
{product.nombre} - S/ {product.ingresos.toFixed(2)}
({product.cantidad} units)
</li>
))}
</ul>
{/* Recent Sales */}
<h2>Recent Transactions</h2>
<ul>
{ventasRecientes.map(sale => (
<li key={sale.id}>
{sale.serie}-{sale.correlativo} - {sale.clienteNombre}
- S/ {sale.total.toFixed(2)}
</li>
))}
</ul>
</div>
);
}
Return Value
Key performance indicator metrics computed from all active sales
Top 5 products ranked by revenue. Includes quantity sold and total revenue per product.
5 most recent sales sorted by emission date, enriched with customer names
Type Definitions
KPIMetrics
interface KPIMetrics {
totalVentas: number; // Total number of active sales
ingresosTotales: number; // Total revenue (sum of all sale.total)
ticketPromedio: number; // Average transaction value
clientesUnicos: number; // Count of unique customers
}
TopProduct
interface TopProduct {
id: string;
nombre: string; // Product name
cantidad: number; // Total units sold
ingresos: number; // Total revenue from this product
}
RecentSale
interface RecentSale {
id: string;
serie: string;
correlativo: string;
customerId: string;
vehicleId: string | null;
tipoComprobante: string;
fechaEmision: string;
total: number;
estadoSunat: string;
clienteNombre: string; // Enriched customer name
// ... other sale fields
}
Features
KPI Calculation
All KPIs are computed in real-time from active (non-deleted) sales:
const { kpis } = useReports();
// kpis.totalVentas: Count of all sales where isDeleted === false
// kpis.ingresosTotales: Sum of all sale.total values
// kpis.ticketPromedio: Average transaction size (ingresosTotales / totalVentas)
// kpis.clientesUnicos: Count of unique customerId values
The hook automatically excludes deleted sales (isDeleted: true) from all calculations to ensure accurate metrics.
Top Products Analysis
Products are ranked by total revenue, not quantity sold:
const { topProductos } = useReports();
// Returns top 5 products by revenue
topProductos.forEach(product => {
console.log(`${product.nombre}: S/ ${product.ingresos} (${product.cantidad} units)`);
});
The algorithm:
- Filters sale details to only include active sales
- Aggregates quantity and revenue by product ID
- Joins with product catalog to get names
- Sorts by revenue descending
- Returns top 5
If a product has been deleted from the catalog but appears in historical sales, it will show as “Producto Eliminado” in the report.
Recent Sales Feed
Recent transactions enriched with customer information:
const { ventasRecientes } = useReports();
// Returns 5 most recent sales with customer names
ventasRecientes.forEach(sale => {
console.log(`${sale.serie}-${sale.correlativo}: ${sale.clienteNombre}`);
});
Sales are sorted by fechaEmision in descending order (newest first).
The hook uses useMemo for all computations to prevent unnecessary recalculations:
const kpis = useMemo(() => {
// Expensive aggregation only runs when db.sales changes
const ventasActivas = db.sales.filter(s => !s.isDeleted);
// ... calculations
}, []); // Empty deps = compute once on mount
const topProductos = useMemo(() => {
// Joins and sorting cached
// ... aggregation logic
}, []); // Empty deps = compute once on mount
Since the dependency array is empty, metrics are computed once on component mount. In a production app with real-time data, you would include data dependencies in the array to trigger recalculation on updates.
Data Sources
The hook aggregates data from multiple mock database tables:
import { db } from '@data/db';
// Tables used:
// - db.sales: Transaction records
// - db.saleDetails: Line items (products per sale)
// - db.customers: Customer directory
// - db.products: Product catalog
Example: Dashboard Component
import { useReports } from '@hooks/useReports';
function Dashboard() {
const { kpis, topProductos, ventasRecientes } = useReports();
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('es-PE', {
style: 'currency',
currency: 'PEN'
}).format(amount);
};
return (
<div className="dashboard">
{/* KPI Cards */}
<div className="kpi-grid">
<div className="kpi-card">
<h3>Total Sales</h3>
<p className="kpi-value">{kpis.totalVentas}</p>
</div>
<div className="kpi-card">
<h3>Total Revenue</h3>
<p className="kpi-value">{formatCurrency(kpis.ingresosTotales)}</p>
</div>
<div className="kpi-card">
<h3>Average Ticket</h3>
<p className="kpi-value">{formatCurrency(kpis.ticketPromedio)}</p>
</div>
<div className="kpi-card">
<h3>Unique Customers</h3>
<p className="kpi-value">{kpis.clientesUnicos}</p>
</div>
</div>
{/* Top Products Chart */}
<div className="top-products">
<h2>Best Selling Products</h2>
{topProductos.map((product, index) => (
<div key={product.id} className="product-row">
<span className="rank">#{index + 1}</span>
<span className="name">{product.nombre}</span>
<span className="revenue">{formatCurrency(product.ingresos)}</span>
<span className="quantity">{product.cantidad} units</span>
</div>
))}
</div>
{/* Recent Activity */}
<div className="recent-sales">
<h2>Recent Transactions</h2>
{ventasRecientes.map(sale => (
<div key={sale.id} className="sale-row">
<span className="doc-number">{sale.serie}-{sale.correlativo}</span>
<span className="customer">{sale.clienteNombre}</span>
<span className="amount">{formatCurrency(sale.total)}</span>
<span className="status">{sale.estadoSunat}</span>
</div>
))}
</div>
</div>
);
}