useCustomers Hook
The useCustomers hook manages the customer directory including search, pagination, CRUD operations, and external API validation for RUC/DNI documents.
Import
import { useCustomers } from '@hooks/useCustomers';
Basic Usage
import { useCustomers } from '@hooks/useCustomers';
function CustomersPage() {
const {
currentCustomers,
searchTerm,
handleSearch,
openModal
} = useCustomers();
return (
<div>
<input
value={searchTerm}
onChange={handleSearch}
placeholder="Search customers..."
/>
{currentCustomers.map(customer => (
<div key={customer.id} onClick={() => openModal('details', customer)}>
{customer.nombreRazonSocial} - {customer.numeroDocumento}
</div>
))}
</div>
);
}
Return Value
Search and Filtering
Current search query string.
handleSearch
(e: React.ChangeEvent<HTMLInputElement>) => void
Handles search input changes and resets pagination to page 1.Searches across:
- Document number (numeroDocumento)
- Name/Business name (nombreRazonSocial)
- Email address (email)
Customer Data
Array of customers for the current page (filtered and paginated).interface Customer {
id: string;
tipoDocumentoIdentidad: string; // 'RUC' | 'DNI' | 'CE'
numeroDocumento: string;
nombreRazonSocial: string;
direccion?: string;
telefono?: string;
email?: string;
// Computed fields
vehiculosPropios: Vehicle[]; // Vehicles owned
vehiculosConducidos: Vehicle[]; // Vehicles driven
totalVehiculosRelacionados: number;
}
Total number of customers after filtering.
Current page number (1-indexed).
Function to change the current page.
Total number of pages based on filtered results and items per page.
Number of customers displayed per page (default: 10).
handleItemsPerPageChange
(e: React.ChangeEvent<HTMLSelectElement>) => void
Changes items per page and resets to page 1.
Modal Management
Currently open modal type.type ModalType = 'details' | 'add' | 'edit' | 'delete' | null;
The customer object currently selected in a modal.
openModal
(type: ModalType, customer?: any) => void
Opens a modal with the specified type and optional customer data.Parameters:
type - The modal type to open
customer (optional) - Customer data to display/edit
Closes the active modal and clears selected customer.
API Validation
true when an external API validation request is in progress.
fetchDecolectaData
(tipoDoc: string, numeroDoc: string) => Promise<object | null>
Validates RUC/DNI via external Decolecta API (simulated).Parameters:
tipoDoc - Document type (‘RUC’, ‘DNI’, ‘CE’)
numeroDoc - Document number to validate
Returns: Promise resolving to:{
nombreRazonSocial: string;
direccion: string;
} | null
Behavior:
- Sets
isSearchingApi to true during request
- Simulates 1.5s network latency
- Returns mock data for valid RUC (11 digits) or DNI (8 digits)
- Returns null for invalid documents
Examples
Complete Customer Management Page
import { useCustomers } from '@hooks/useCustomers';
import { Input } from '@components/ui/Input';
import { Select } from '@components/ui/Select';
import { Button } from '@components/ui/Button';
function CustomersPage() {
const {
currentCustomers,
searchTerm,
handleSearch,
currentPage,
totalPages,
setCurrentPage,
itemsPerPage,
handleItemsPerPageChange,
totalItems,
openModal
} = useCustomers();
return (
<div>
{/* Search and Controls */}
<div>
<Input
placeholder="Search by document, name, or email..."
value={searchTerm}
onChange={handleSearch}
/>
<Select value={itemsPerPage} onChange={handleItemsPerPageChange}>
<option value={10}>10 per page</option>
<option value={25}>25 per page</option>
<option value={50}>50 per page</option>
</Select>
<Button onClick={() => openModal('add')}>
Add Customer
</Button>
</div>
{/* Results Summary */}
<p>Showing {currentCustomers.length} of {totalItems} customers</p>
{/* Customer Table */}
<table>
<thead>
<tr>
<th>Document</th>
<th>Name</th>
<th>Contact</th>
<th>Vehicles</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{currentCustomers.map(customer => (
<tr key={customer.id}>
<td>
{customer.tipoDocumentoIdentidad}: {customer.numeroDocumento}
</td>
<td>{customer.nombreRazonSocial}</td>
<td>
{customer.telefono}<br/>
{customer.email}
</td>
<td>{customer.totalVehiculosRelacionados} vehicles</td>
<td>
<Button
size="sm"
onClick={() => openModal('details', customer)}
>
Details
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => openModal('edit', customer)}
>
Edit
</Button>
<Button
size="sm"
variant="danger"
onClick={() => openModal('delete', customer)}
>
Delete
</Button>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div>
<Button
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
Previous
</Button>
<span>Page {currentPage} of {totalPages}</span>
<Button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
>
Next
</Button>
</div>
</div>
);
}
import { useState } from 'react';
import { useCustomers } from '@hooks/useCustomers';
import { Input } from '@components/ui/Input';
import { Select } from '@components/ui/Select';
import { Button } from '@components/ui/Button';
function CustomerForm() {
const { fetchDecolectaData, isSearchingApi } = useCustomers();
const [formData, setFormData] = useState({
tipoDocumentoIdentidad: 'RUC',
numeroDocumento: '',
nombreRazonSocial: '',
direccion: '',
telefono: '',
email: ''
});
const [apiSuccess, setApiSuccess] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSearchApi = async () => {
const result = await fetchDecolectaData(
formData.tipoDocumentoIdentidad,
formData.numeroDocumento
);
if (result) {
setFormData({
...formData,
nombreRazonSocial: result.nombreRazonSocial,
direccion: result.direccion
});
setApiSuccess(true);
}
};
return (
<form>
<div>
<label>Document Type</label>
<Select
name="tipoDocumentoIdentidad"
value={formData.tipoDocumentoIdentidad}
onChange={handleChange}
>
<option value="RUC">RUC (Business)</option>
<option value="DNI">DNI (Individual)</option>
<option value="CE">Foreign ID</option>
</Select>
</div>
<div>
<label>
Document Number
{apiSuccess && <span className="text-green-600">✓ Verified</span>}
</label>
<div style={{ display: 'flex' }}>
<Input
name="numeroDocumento"
value={formData.numeroDocumento}
onChange={handleChange}
maxLength={formData.tipoDocumentoIdentidad === 'RUC' ? 11 : 8}
/>
<Button
type="button"
onClick={handleSearchApi}
disabled={isSearchingApi || formData.numeroDocumento.length < 8}
isLoading={isSearchingApi}
>
Validate
</Button>
</div>
</div>
<div>
<label>Name / Business Name</label>
<Input
name="nombreRazonSocial"
value={formData.nombreRazonSocial}
onChange={handleChange}
/>
</div>
<div>
<label>Address</label>
<Input
name="direccion"
value={formData.direccion}
onChange={handleChange}
/>
</div>
<div>
<label>Phone</label>
<Input
name="telefono"
value={formData.telefono}
onChange={handleChange}
/>
</div>
<div>
<label>Email</label>
<Input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<Button type="submit">Save Customer</Button>
</form>
);
}
Viewing Customer’s Vehicles
import { useCustomers } from '@hooks/useCustomers';
function CustomerVehiclesList({ customer }) {
if (!customer) return null;
return (
<div>
<h3>Vehicles Owned</h3>
{customer.vehiculosPropios.length > 0 ? (
<ul>
{customer.vehiculosPropios.map(vehicle => (
<li key={vehicle.id}>
{vehicle.placa} - {vehicle.marca} {vehicle.modelo}
</li>
))}
</ul>
) : (
<p>No vehicles owned</p>
)}
<h3>Vehicles Driven</h3>
{customer.vehiculosConducidos.length > 0 ? (
<ul>
{customer.vehiculosConducidos.map(vehicle => (
<li key={vehicle.id}>
{vehicle.placa} - {vehicle.marca} {vehicle.modelo}
</li>
))}
</ul>
) : (
<p>No vehicles driven</p>
)}
</div>
);
}
Delete Confirmation
import { useCustomers } from '@hooks/useCustomers';
import { CustomerDeleteModal } from '@components/customers/CustomerDeleteModal';
function CustomerDeleteFlow() {
const {
selectedCustomer,
activeModal,
closeModal
} = useCustomers();
const handleDelete = () => {
// Perform delete operation
console.log('Deleting customer:', selectedCustomer.id);
// API call here
closeModal();
};
return (
<CustomerDeleteModal
isOpen={activeModal === 'delete'}
onClose={closeModal}
onConfirm={handleDelete}
customer={selectedCustomer}
/>
);
}
API Validation Details
Decolecta API Integration
The hook simulates integration with Decolecta API for SUNAT/RENIEC validation:
// Mock responses for different document types
if (tipoDoc === 'DNI' && numeroDoc.length === 8) {
return {
nombreRazonSocial: "MARIO ALBERTO ROJAS PEREZ",
direccion: "CALLE LOS PINOS 123, LIMA"
};
}
if (tipoDoc === 'RUC' && numeroDoc.length === 11) {
return {
nombreRazonSocial: "MINERA LOS ANDES S.A.A.",
direccion: "AV. PRINCIPAL 456 ZONA INDUSTRIAL, AREQUIPA"
};
}
Production Implementation
In production, replace with actual API call:
const fetchDecolectaData = async (tipoDoc: string, numeroDoc: string) => {
const token = import.meta.env.VITE_DECOLECTA_TOKEN;
const response = await fetch(`https://api.decolecta.com/validate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ tipo: tipoDoc, numero: numeroDoc })
});
return response.ok ? await response.json() : null;
};
Vehicle Relationships
The hook automatically computes vehicle relationships:
// Vehicles owned by customer (as propietario)
vehiculosPropios = vehicles.filter(v =>
v.propietarioId === customer.id && !v.isDeleted
);
// Vehicles driven by customer (as conductor)
vehiculosConducidos = vehicles.filter(v =>
v.conductorHabitualId === customer.id && !v.isDeleted
);
// Total count
totalVehiculosRelacionados = vehiculosPropios.length + vehiculosConducidos.length;
Document Types
type DocumentType =
| 'RUC' // Tax ID for businesses (11 digits)
| 'DNI' // National ID for individuals (8 digits)
| 'CE'; // Foreign ID card
The API validation simulates a 1.5 second network delay to mimic real-world behavior during development.
Use the fetchDecolectaData function to auto-fill customer name and address from government databases, reducing manual data entry.
In production, ensure proper error handling for API failures and implement rate limiting to avoid excessive API calls.
Source Code
Location: /home/daytona/workspace/source/src/hooks/useCustomers.ts:1