useSales Hook
The useSales hook manages the complete sales workflow including document setup, shopping cart, product search, and calculations. It handles pre-filled data from vehicle history for quick repeat services.
Import
import { useSales } from '@hooks/useSales';
Basic Usage
import { useSales } from '@hooks/useSales';
function SalesPage() {
const {
documento,
cart,
addItem,
removeItem,
totals
} = useSales();
return (
<div>
<h2>{documento.tipo} {documento.serie}-{documento.correlativo}</h2>
<div>
{cart.map(item => (
<div key={item.tempId}>
{item.nombre} - S/ {item.precioUnitario} x {item.cantidad}
<button onClick={() => removeItem(item.tempId)}>Remove</button>
</div>
))}
</div>
<p>Total: S/ {totals.total.toFixed(2)}</p>
</div>
);
}
Return Value
Document State
Current document information.{
tipo: string; // 'FACTURA' or 'BOLETA'
serie: string; // Document series (e.g., 'F001')
correlativo: string; // Sequential number (e.g., '000123')
fecha: string; // Date in YYYY-MM-DD format
cliente: Customer | null; // Selected customer
}
Function to update document state.
Additional sale information.{
placa: string; // Vehicle license plate
ordenCompra: string; // Purchase order number
observaciones: string; // Notes/observations
condicionPago: string; // Payment terms (e.g., 'CONTADO')
metodoPago: string; // Payment method (e.g., 'EFECTIVO')
kilometrajeActual: string; // Current mileage
proximoCambioKm: string; // Next service mileage
}
handleExtraChange
(key: string, value: string) => void
Updates a specific extra field.Parameters:
key - The field name to update
value - The new value
Array of items in the shopping cart.interface CartItem {
id: string; // Product ID
tempId: string; // Unique temp ID for cart (UUID)
nombre: string; // Product name
cantidad: number; // Quantity
precioUnitario: number; // Unit price
// ... other product properties
}
Adds a product to the cart or increments quantity if already present.Parameters:
product - Product object to add
Behavior:
- If product already in cart, increments quantity
- If new product, adds with quantity 1
- Clears product search after adding
Removes an item from the cart.Parameters:
tempId - The temporary ID of the cart item
updateQuantity
(tempId: string, qty: number) => void
Updates the quantity of a cart item.Parameters:
tempId - The temporary ID of the cart item
qty - New quantity (minimum 1)
Product Search
Current product search query.
Updates the product search query.
Suggested products based on vehicle history (from prefill data).
Calculations
Calculated totals for the sale.{
subtotal: number; // Total without tax (total / 1.18)
igv: number; // Tax amount (18% in Peru)
total: number; // Grand total including tax
}
Automatically recalculates when cart changes.
Object tracking which fields are in inline edit mode.
Activates inline editing for a field.
Deactivates inline editing for a field.
Examples
import { useSales } from '@hooks/useSales';
import { Input } from '@components/ui/Input';
import { Select } from '@components/ui/Select';
import { Button } from '@components/ui/Button';
function SalesForm() {
const {
documento,
setDocumento,
extras,
handleExtraChange,
cart,
addItem,
removeItem,
updateQuantity,
totals,
productSearch,
setProductSearch
} = useSales();
return (
<div>
{/* Document Header */}
<div>
<Select
value={documento.tipo}
onChange={(e) => setDocumento({ ...documento, tipo: e.target.value })}
>
<option value="FACTURA">Factura</option>
<option value="BOLETA">Boleta</option>
</Select>
<Input
value={documento.fecha}
type="date"
onChange={(e) => setDocumento({ ...documento, fecha: e.target.value })}
/>
</div>
{/* Extra Fields */}
<div>
<Input
placeholder="Placa del Vehículo"
value={extras.placa}
onChange={(e) => handleExtraChange('placa', e.target.value)}
/>
<Input
placeholder="Kilometraje Actual"
value={extras.kilometrajeActual}
onChange={(e) => handleExtraChange('kilometrajeActual', e.target.value)}
/>
</div>
{/* Shopping Cart */}
<div>
{cart.map(item => (
<div key={item.tempId}>
<span>{item.nombre}</span>
<Input
type="number"
value={item.cantidad}
onChange={(e) => updateQuantity(item.tempId, Number(e.target.value))}
/>
<span>S/ {(item.precioUnitario * item.cantidad).toFixed(2)}</span>
<Button
variant="danger"
size="sm"
onClick={() => removeItem(item.tempId)}
>
Remove
</Button>
</div>
))}
</div>
{/* Totals */}
<div>
<p>Subtotal: S/ {totals.subtotal.toFixed(2)}</p>
<p>IGV (18%): S/ {totals.igv.toFixed(2)}</p>
<p><strong>Total: S/ {totals.total.toFixed(2)}</strong></p>
</div>
</div>
);
}
Product Search and Add
import { useSales } from '@hooks/useSales';
import { db } from '@data/db';
function ProductSelector() {
const { productSearch, setProductSearch, addItem } = useSales();
const filteredProducts = db.products.filter(p =>
p.nombre.toLowerCase().includes(productSearch.toLowerCase())
);
return (
<div>
<Input
placeholder="Buscar productos..."
value={productSearch}
onChange={(e) => setProductSearch(e.target.value)}
/>
{productSearch && (
<div>
{filteredProducts.map(product => (
<div key={product.id} onClick={() => addItem(product)}>
{product.nombre} - S/ {product.precioVenta}
</div>
))}
</div>
)}
</div>
);
}
Pre-filled from Vehicle History
import { useNavigate } from 'react-router-dom';
// In VehiclesPage
function handleRepeatService(vehicle, pastSale) {
navigate('/sales', {
state: {
prefillData: {
vehicleId: vehicle.id,
customerId: vehicle.propietarioId,
placa: vehicle.placa,
cartItems: pastSale.items, // Pre-fill cart
observacionSugerida: 'Repeated service',
kilometrajeActual: vehicle.kmActual,
kmProximo: pastSale.proximoCambioKm
}
}
});
}
// In SalesPage - hook automatically reads prefillData
function SalesPage() {
const { cart, extras } = useSales();
// Cart and extras are automatically pre-filled from location.state
console.log(cart); // Contains items from pastSale
console.log(extras.placa); // Contains vehicle plate
}
Inline Editing for Fields
import { useSales } from '@hooks/useSales';
function InlineEditableField() {
const {
extras,
handleExtraChange,
inlineInputs,
toggleInlineInput,
blurInlineInput
} = useSales();
return (
<div>
{inlineInputs.observaciones ? (
<Input
value={extras.observaciones}
onChange={(e) => handleExtraChange('observaciones', e.target.value)}
onBlur={() => blurInlineInput('observaciones')}
autoFocus
/>
) : (
<span onClick={() => toggleInlineInput('observaciones')}>
{extras.observaciones || 'Click to add observations'}
</span>
)}
</div>
);
}
Payment Method Selection
import { useSales } from '@hooks/useSales';
import { Select } from '@components/ui/Select';
function PaymentSelector() {
const { extras, handleExtraChange } = useSales();
return (
<div>
<label>Método de Pago</label>
<Select
value={extras.metodoPago}
onChange={(e) => handleExtraChange('metodoPago', e.target.value)}
>
<option value="EFECTIVO">Efectivo</option>
<option value="TARJETA">Tarjeta</option>
<option value="TRANSFERENCIA">Transferencia</option>
<option value="YAPE">Yape</option>
</Select>
<label>Condición de Pago</label>
<Select
value={extras.condicionPago}
onChange={(e) => handleExtraChange('condicionPago', e.target.value)}
>
<option value="CONTADO">Contado</option>
<option value="CREDITO_30">Crédito 30 días</option>
<option value="CREDITO_60">Crédito 60 días</option>
</Select>
</div>
);
}
Prefill Data Structure
When navigating from vehicles page, the hook accepts this prefill structure:
interface PrefillData {
vehicleId?: string;
customerId?: string;
choferId?: string;
placa?: string;
cartItems?: CartItem[]; // Pre-populate cart
observacionSugerida?: string;
kilometrajeActual?: string;
kmProximo?: string;
productosUsuales?: Product[]; // Suggested products
}
Tax Calculation
The hook uses Peru’s 18% IGV (VAT) tax:
// From cart total (includes tax)
const total = cart.reduce((acc, item) =>
acc + (item.precioUnitario * item.cantidad), 0
);
// Calculate base and tax
const subtotal = total / 1.18;
const igv = total - subtotal;
State Persistence
Cart items use temporary UUIDs for React keys:
{
...product,
tempId: crypto.randomUUID(), // Unique ID for cart item
cantidad: 1,
precioUnitario: product.precioVenta
}
The hook automatically reads location.state.prefillData from React Router to pre-populate form fields and cart items.
Ensure product prices include tax (IGV) as the calculation divides by 1.18 to extract the base amount.
Source Code
Location: /home/daytona/workspace/source/src/hooks/useSales.ts:1