Skip to main content

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

searchTerm
string
Current search query string.
Handles search input changes and resets pagination to page 1.Searches across:
  • Document number (numeroDocumento)
  • Name/Business name (nombreRazonSocial)
  • Email address (email)

Customer Data

currentCustomers
array
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;
}
totalItems
number
Total number of customers after filtering.

Pagination

currentPage
number
Current page number (1-indexed).
setCurrentPage
(page: number) => void
Function to change the current page.
totalPages
number
Total number of pages based on filtered results and items per page.
itemsPerPage
number
Number of customers displayed per page (default: 10).
handleItemsPerPageChange
(e: React.ChangeEvent<HTMLSelectElement>) => void
Changes items per page and resets to page 1.
activeModal
ModalType
Currently open modal type.
type ModalType = 'details' | 'add' | 'edit' | 'delete' | null;
selectedCustomer
any | 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
closeModal
() => void
Closes the active modal and clears selected customer.

API Validation

isSearchingApi
boolean
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>
  );
}

Customer Form with API Validation

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

Build docs developers (and LLMs) love