Skip to main content
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

kpis
KPIMetrics
Key performance indicator metrics computed from all active sales
topProductos
TopProduct[]
Top 5 products ranked by revenue. Includes quantity sold and total revenue per product.
ventasRecientes
RecentSale[]
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:
  1. Filters sale details to only include active sales
  2. Aggregates quantity and revenue by product ID
  3. Joins with product catalog to get names
  4. Sorts by revenue descending
  5. 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).

Performance Optimization

The hook uses useMemo for all computations to prevent unnecessary recalculations:
src/hooks/useReports.ts
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>
  );
}

Build docs developers (and LLMs) love