diff --git a/src/features/clientes/api/clientes.api.ts b/src/features/clientes/api/clientes.api.ts new file mode 100644 index 0000000..008c197 --- /dev/null +++ b/src/features/clientes/api/clientes.api.ts @@ -0,0 +1,57 @@ +import { api } from '@/services/api/axios-instance'; +import type { + Cliente, + ClienteFilters, + ClientesResponse, + CreateClienteDto, + UpdateClienteDto, +} from '../types'; + +const BASE_URL = '/api/v1/clientes'; + +export const clientesApi = { + // Get all clientes with filters + getAll: async (filters?: ClienteFilters): Promise => { + const params = new URLSearchParams(); + if (filters?.search) params.append('search', filters.search); + if (filters?.tipo) params.append('tipo', filters.tipo); + if (filters?.estado) params.append('estado', filters.estado); + if (filters?.activo !== undefined) params.append('activo', String(filters.activo)); + if (filters?.limit) params.append('limit', String(filters.limit)); + if (filters?.offset) params.append('offset', String(filters.offset)); + + const response = await api.get(`${BASE_URL}?${params.toString()}`); + return response.data; + }, + + // Get cliente by ID + getById: async (id: string): Promise<{ data: Cliente }> => { + const response = await api.get<{ data: Cliente }>(`${BASE_URL}/${id}`); + return response.data; + }, + + // Search clientes (for autocomplete) + search: async (query: string, limit = 10): Promise<{ data: Cliente[] }> => { + const response = await api.get<{ data: Cliente[] }>( + `${BASE_URL}/search?q=${encodeURIComponent(query)}&limit=${limit}` + ); + return response.data; + }, + + // Create cliente + create: async (data: CreateClienteDto): Promise<{ data: Cliente }> => { + const response = await api.post<{ data: Cliente }>(BASE_URL, data); + return response.data; + }, + + // Update cliente + update: async (id: string, data: UpdateClienteDto): Promise<{ data: Cliente }> => { + const response = await api.patch<{ data: Cliente }>(`${BASE_URL}/${id}`, data); + return response.data; + }, + + // Delete cliente + delete: async (id: string): Promise => { + await api.delete(`${BASE_URL}/${id}`); + }, +}; diff --git a/src/features/clientes/index.ts b/src/features/clientes/index.ts new file mode 100644 index 0000000..2e6addf --- /dev/null +++ b/src/features/clientes/index.ts @@ -0,0 +1,5 @@ +// Types +export * from './types'; + +// API +export { clientesApi } from './api/clientes.api'; diff --git a/src/features/clientes/types/index.ts b/src/features/clientes/types/index.ts new file mode 100644 index 0000000..9a35af5 --- /dev/null +++ b/src/features/clientes/types/index.ts @@ -0,0 +1,82 @@ +/** + * Cliente Types + */ + +export enum TipoCliente { + SHIPPER = 'SHIPPER', + CONSIGNEE = 'CONSIGNEE', + SHIPPER_CONSIGNEE = 'SHIPPER_CONSIGNEE', + BROKER = 'BROKER', +} + +export enum EstadoCliente { + ACTIVO = 'ACTIVO', + INACTIVO = 'INACTIVO', + SUSPENDIDO = 'SUSPENDIDO', + BLOQUEADO = 'BLOQUEADO', +} + +export interface Cliente { + id: string; + tenantId: string; + codigo: string; + razonSocial: string; + nombreComercial?: string; + rfc?: string; + tipo: TipoCliente; + estado: EstadoCliente; + // Direccion fiscal + direccionFiscal?: string; + codigoPostal?: string; + ciudad?: string; + estadoRepublica?: string; + pais?: string; + // Contacto + telefono?: string; + email?: string; + contactoPrincipal?: string; + // Credito + creditoLimite?: number; + creditoUtilizado?: number; + diasCredito?: number; + // Configuracion + tipoTarifa?: string; + condicionesPago?: string; + observaciones?: string; + activo: boolean; + createdAt: string; + updatedAt: string; +} + +export interface ClienteFilters { + search?: string; + tipo?: TipoCliente; + estado?: EstadoCliente; + activo?: boolean; + limit?: number; + offset?: number; +} + +export interface ClientesResponse { + data: Cliente[]; + total: number; +} + +export interface CreateClienteDto { + razonSocial: string; + nombreComercial?: string; + rfc?: string; + tipo?: TipoCliente; + direccionFiscal?: string; + codigoPostal?: string; + ciudad?: string; + estadoRepublica?: string; + pais?: string; + telefono?: string; + email?: string; + contactoPrincipal?: string; +} + +export interface UpdateClienteDto extends Partial { + estado?: EstadoCliente; +} diff --git a/src/features/ordenes-transporte/components/AsignarUnidadModal.tsx b/src/features/ordenes-transporte/components/AsignarUnidadModal.tsx new file mode 100644 index 0000000..5dee66d --- /dev/null +++ b/src/features/ordenes-transporte/components/AsignarUnidadModal.tsx @@ -0,0 +1,416 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { unidadesApi } from '../../flota/api/unidades.api'; +import { operadoresApi } from '../../flota/api/operadores.api'; +import { viajesApi } from '../../viajes/api/viajes.api'; +import { UnidadStatusBadge } from '../../flota/components/UnidadStatusBadge'; +import { OperadorStatusBadge } from '../../flota/components/OperadorStatusBadge'; +import type { Unidad, Operador } from '../../flota/types'; + +const asignacionSchema = z.object({ + unidadId: z.string().min(1, 'Seleccione una unidad'), + operadorId: z.string().min(1, 'Seleccione un operador'), + fechaSalida: z.string().min(1, 'Fecha de salida es requerida'), + horaSalida: z.string().optional(), + fechaEntrega: z.string().optional(), + notas: z.string().optional(), +}); + +type AsignacionFormData = z.infer; + +interface AsignarUnidadModalProps { + ordenId: string; + ordenNumero: string; + onClose: () => void; + onSuccess: () => void; +} + +export function AsignarUnidadModal({ + ordenId, + ordenNumero, + onClose, + onSuccess, +}: AsignarUnidadModalProps) { + const queryClient = useQueryClient(); + const [step, setStep] = useState<'unidad' | 'operador' | 'fechas'>('unidad'); + const [selectedUnidad, setSelectedUnidad] = useState(null); + const [selectedOperador, setSelectedOperador] = useState(null); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + setValue, + } = useForm({ + resolver: zodResolver(asignacionSchema), + defaultValues: { + fechaSalida: new Date().toISOString().split('T')[0], + }, + }); + + // Fetch unidades disponibles + const { data: unidadesData, isLoading: loadingUnidades } = useQuery({ + queryKey: ['unidades-disponibles'], + queryFn: () => unidadesApi.getDisponibles(), + }); + + // Fetch operadores disponibles + const { data: operadoresData, isLoading: loadingOperadores } = useQuery({ + queryKey: ['operadores-disponibles'], + queryFn: () => operadoresApi.getDisponibles(), + enabled: step !== 'unidad', + }); + + const unidades = unidadesData?.data || []; + const operadores = operadoresData?.data || []; + + // Mutation para crear viaje y asignar + const asignarMutation = useMutation({ + mutationFn: async (data: AsignacionFormData) => { + // Crear viaje con la OT + const viajeData = { + ordenTransporteId: ordenId, + unidadId: data.unidadId, + operadorId: data.operadorId, + fechaSalidaProgramada: data.fechaSalida + (data.horaSalida ? `T${data.horaSalida}:00` : 'T08:00:00'), + fechaEntregaProgramada: data.fechaEntrega || undefined, + notas: data.notas, + }; + return viajesApi.create(viajeData); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ordenes-transporte'] }); + queryClient.invalidateQueries({ queryKey: ['viajes'] }); + queryClient.invalidateQueries({ queryKey: ['unidades-disponibles'] }); + queryClient.invalidateQueries({ queryKey: ['operadores-disponibles'] }); + onSuccess(); + }, + }); + + const handleSelectUnidad = (unidad: Unidad) => { + setSelectedUnidad(unidad); + setValue('unidadId', unidad.id); + setStep('operador'); + }; + + const handleSelectOperador = (operador: Operador) => { + setSelectedOperador(operador); + setValue('operadorId', operador.id); + setStep('fechas'); + }; + + const onSubmit = async (data: AsignacionFormData) => { + await asignarMutation.mutateAsync(data); + }; + + return ( +
+
+
+
+ {/* Header */} +
+
+
+

Asignar Unidad y Operador

+

Orden: {ordenNumero}

+
+ +
+ + {/* Steps Indicator */} +
+
+
+ {selectedUnidad ? '1' : '1'} +
+ + Unidad + +
+
+
+
+ 2 +
+ + Operador + +
+
+
+
+ 3 +
+ + Fechas + +
+
+
+ + {/* Content */} +
+ {/* Step 1: Seleccionar Unidad */} + {step === 'unidad' && ( +
+

Seleccionar Unidad Disponible

+ {loadingUnidades ? ( +
+ + + + +

Cargando unidades...

+
+ ) : unidades.length === 0 ? ( +
+ + + + +

No hay unidades disponibles

+
+ ) : ( +
+ {unidades.map((unidad) => ( +
handleSelectUnidad(unidad)} + className={`cursor-pointer rounded-lg border p-4 transition-colors ${ + selectedUnidad?.id === unidad.id + ? 'border-primary-500 bg-primary-50' + : 'border-gray-200 hover:border-primary-300 hover:bg-gray-50' + }`} + > +
+
+
{unidad.numeroEconomico}
+
+ {unidad.marca} {unidad.modelo} {unidad.anio} - {unidad.tipo?.replace(/_/g, ' ')} +
+
Placas: {unidad.placa || unidad.placas || '-'}
+
+
+ +
{unidad.odometroActual?.toLocaleString()} km
+
+
+
+ ))} +
+ )} +
+ )} + + {/* Step 2: Seleccionar Operador */} + {step === 'operador' && ( +
+
+

Seleccionar Operador Disponible

+ +
+ {selectedUnidad && ( +
+ Unidad seleccionada: + {selectedUnidad.numeroEconomico} +
+ )} + {loadingOperadores ? ( +
+ + + + +

Cargando operadores...

+
+ ) : operadores.length === 0 ? ( +
+ + + +

No hay operadores disponibles

+
+ ) : ( +
+ {operadores.map((operador) => ( +
handleSelectOperador(operador)} + className={`cursor-pointer rounded-lg border p-4 transition-colors ${ + selectedOperador?.id === operador.id + ? 'border-primary-500 bg-primary-50' + : 'border-gray-200 hover:border-primary-300 hover:bg-gray-50' + }`} + > +
+
+
+ {operador.nombre} {operador.apellidoPaterno} {operador.apellidoMaterno} +
+
+ {operador.numeroEmpleado} - Licencia {operador.tipoLicencia} +
+ {operador.telefono && ( +
Tel: {operador.telefono}
+ )} +
+
+ +
{operador.totalViajes} viajes
+
+
+
+ ))} +
+ )} +
+ )} + + {/* Step 3: Fechas y Confirmacion */} + {step === 'fechas' && ( +
+
+

Programar Viaje

+ +
+ + {/* Resumen de seleccion */} +
+
+ Unidad: + {selectedUnidad?.numeroEconomico} +
+
+ Operador: + + {selectedOperador?.nombre} {selectedOperador?.apellidoPaterno} + +
+
+ + {/* Fecha y hora de salida */} +
+
+ + + {errors.fechaSalida && ( +

{errors.fechaSalida.message}

+ )} +
+
+ + +
+
+ + {/* Fecha de entrega */} +
+ + +
+ + {/* Notas */} +
+ +