Modal Components
MotorDesk uses modal dialogs for various user interactions including forms, detail views, and delete confirmations. All modals follow a consistent pattern with overlay, content area, header, body, and footer sections.
Modal Architecture
All modals in MotorDesk share a common structure:
- Overlay - Semi-transparent background that covers the main content
- Modal Content - The main dialog box
- Header - Title and close button
- Body - Main content area (forms, details, etc.)
- Footer - Action buttons
Common Modal Props
Most modals share these common props:
Controls whether the modal is visible.
Callback function called when the modal should close (clicking overlay or close button).
Modal for adding or editing customer information with API validation support.
Import
import { CustomerFormModal } from '@components/customers/CustomerFormModal';
Props
Controls modal visibility.
Called when the modal should close.
Determines if the modal is for adding a new customer or editing an existing one.
Object containing the current form field values.
Handler for form field changes.
Handler for API lookup button (validates RUC/DNI via external API).
Indicates if API search is in progress.
Indicates if API validation was successful.
Usage Example
import { useState } from 'react';
import { CustomerFormModal } from '@components/customers/CustomerFormModal';
function CustomersPage() {
const [isOpen, setIsOpen] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const [apiSuccess, setApiSuccess] = useState(false);
const [formData, setFormData] = useState({
tipoDocumentoIdentidad: 'RUC',
numeroDocumento: '',
nombreRazonSocial: '',
direccion: '',
telefono: '',
email: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSearchApi = async () => {
setIsSearching(true);
// Call external API for RUC/DNI validation
const result = await validateDocument(formData.numeroDocumento);
if (result) {
setFormData({
...formData,
nombreRazonSocial: result.name,
direccion: result.address
});
setApiSuccess(true);
}
setIsSearching(false);
};
return (
<>
<button onClick={() => setIsOpen(true)}>Add Customer</button>
<CustomerFormModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
mode="add"
formData={formData}
onChange={handleChange}
onSearchApi={handleSearchApi}
isSearching={isSearching}
apiSuccess={apiSuccess}
/>
</>
);
}
The CustomerFormModal includes these fields:
- Tipo de Documento - Select dropdown (RUC, DNI, CE)
- Número de Documento - Text input with API search button
- Nombres o Razón Social - Text input (can be auto-filled by API)
- Dirección Fiscal - Text input (can be auto-filled by API)
- Teléfono - Text input
- Email - Text input
VehicleDetailsModal
Modal displaying complete vehicle information and service history.
Import
import { VehicleDetailsModal } from '@components/vehicles/VehicleDetailsModal';
Props
The vehicle object containing all vehicle data and history.
Called when the modal should close.
onEmitFactura
(vehicle: any, pastSale?: any) => void
required
Called when user wants to create a new sale or repeat a previous service.
Usage Example
import { useState } from 'react';
import { VehicleDetailsModal } from '@components/vehicles/VehicleDetailsModal';
function VehiclesPage() {
const [selectedVehicle, setSelectedVehicle] = useState(null);
const handleEmitFactura = (vehicle: any, pastSale?: any) => {
// Navigate to sales page with pre-filled data
navigate('/sales', {
state: {
prefillData: {
vehicleId: vehicle.id,
customerId: vehicle.propietarioId,
placa: vehicle.placa,
cartItems: pastSale?.items || [],
kilometrajeActual: vehicle.kmActual
}
}
});
};
return (
<>
<button onClick={() => setSelectedVehicle(vehicle)}>
View Details
</button>
{selectedVehicle && (
<VehicleDetailsModal
vehicle={selectedVehicle}
onClose={() => setSelectedVehicle(null)}
onEmitFactura={handleEmitFactura}
/>
)}
</>
);
}
Vehicle Object Structure
interface Vehicle {
id: string;
placa: string;
marca: string;
modelo: string;
anio: number;
color?: string;
clienteNombre: string;
clienteDocumento: string;
choferNombre?: string;
kmActual: number;
kmProximo: number;
historial: SaleHistory[];
}
interface SaleHistory {
id: string;
tipoComprobante: string;
serie: string;
correlativo: string;
fechaEmision: string;
total: number;
kilometrajeIngreso: number;
proximoCambioKm: number;
items: SaleItem[];
}
Features
- Displays vehicle information (owner, specs, current mileage)
- Shows complete service history chronologically
- Each service shows products/services performed
- Tracks mileage at each service
- “Repeat Service” button to recreate past sales
- “New Sale” button to create blank invoice
CustomerDeleteModal
Confirmation modal for deleting customers.
Import
import { CustomerDeleteModal } from '@components/customers/CustomerDeleteModal';
Props
Controls modal visibility.
Called when user cancels deletion.
Called when user confirms deletion.
The customer object to be deleted.
Usage Example
import { useState } from 'react';
import { CustomerDeleteModal } from '@components/customers/CustomerDeleteModal';
function CustomersList() {
const [customerToDelete, setCustomerToDelete] = useState(null);
const handleConfirmDelete = () => {
// Perform delete operation
deleteCustomer(customerToDelete.id);
setCustomerToDelete(null);
};
return (
<>
<button onClick={() => setCustomerToDelete(customer)}>
Delete Customer
</button>
<CustomerDeleteModal
isOpen={!!customerToDelete}
onClose={() => setCustomerToDelete(null)}
onConfirm={handleConfirmDelete}
customer={customerToDelete}
/>
</>
);
}
Features
- Warning icon and message
- Displays customer name to be deleted
- Warning text about irreversible action
- Cancel and Confirm buttons
- Rounded design with visual hierarchy
Modal Best Practices
Closing Behavior
Modals should close when:
- User clicks the close (X) button
- User clicks outside the modal (on overlay)
- User presses Escape key (implement with useEffect)
- User completes the action successfully
import { useEffect } from 'react';
function useEscapeKey(onClose: () => void, isOpen: boolean) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose, isOpen]);
}
Preventing Propagation
Always prevent click events on modal content from propagating to the overlay:
<div className="modalOverlay" onClick={onClose}>
<div className="modalContent" onClick={(e) => e.stopPropagation()}>
{/* Modal content */}
</div>
</div>
Conditional Rendering
Return null when modal is closed to improve performance:
function MyModal({ isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="modalOverlay">
{/* Modal content */}
</div>
);
}
Styling
Modals use CSS modules from @styles/modules/ with consistent classes:
modalOverlay - Full-screen overlay with semi-transparent background
modalContent - Centered dialog box with shadow
modalHeader - Title bar with close button
modalBody - Scrollable content area
modalFooter - Action buttons area
Accessibility
- Focus trap within modal when open
- Escape key to close
- Proper heading hierarchy
- ARIA labels for screen readers
- Keyboard navigation support
Always provide a way to close modals without completing the action (close button, overlay click, or cancel button).
Source Code
- CustomerFormModal:
/home/daytona/workspace/source/src/components/customers/CustomerFormModal.tsx:1
- VehicleDetailsModal:
/home/daytona/workspace/source/src/components/vehicles/VehicleDetailsModal.tsx:1
- CustomerDeleteModal:
/home/daytona/workspace/source/src/components/customers/CustomerDeleteModal.tsx:1