erp-mecanicas-diesel-fronte.../src/pages/Diagnostics.tsx

280 lines
12 KiB
TypeScript

import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import {
Stethoscope,
Plus,
Search,
Filter,
CheckCircle2,
XCircle,
AlertTriangle,
Loader2,
AlertCircle,
Truck,
Calendar,
Cpu,
Gauge,
Cog,
Eye,
} from 'lucide-react';
import { diagnosticsApi } from '../services/api/diagnostics';
import type { Diagnostic, DiagnosticResult, DiagnosticFilters } from '../services/api/diagnostics';
import type { DiagnosticType } from '../types';
const DIAGNOSTIC_TYPES: { value: DiagnosticType; label: string; icon: typeof Cpu }[] = [
{ value: 'obd_scanner', label: 'Scanner OBD', icon: Cpu },
{ value: 'injector_bench', label: 'Banco Inyectores', icon: Gauge },
{ value: 'pump_bench', label: 'Banco Bomba', icon: Cog },
{ value: 'measurements', label: 'Mediciones', icon: Stethoscope },
];
const RESULT_CONFIG: Record<DiagnosticResult, { label: string; color: string; icon: typeof CheckCircle2 }> = {
pass: { label: 'Aprobado', color: 'bg-green-100 text-green-700', icon: CheckCircle2 },
fail: { label: 'Fallo', color: 'bg-red-100 text-red-700', icon: XCircle },
needs_attention: { label: 'Requiere Atencion', color: 'bg-yellow-100 text-yellow-700', icon: AlertTriangle },
};
export function DiagnosticsPage() {
const [filters, setFilters] = useState<DiagnosticFilters>({ page: 1, pageSize: 20 });
const [searchTerm, setSearchTerm] = useState('');
// Fetch diagnostics
const { data, isLoading, error } = useQuery({
queryKey: ['diagnostics', filters],
queryFn: () => diagnosticsApi.list(filters),
});
const diagnostics = data?.data?.data || [];
const handleSearch = () => {
setFilters({ ...filters, search: searchTerm, page: 1 });
};
const handleTypeFilter = (type: DiagnosticType | undefined) => {
setFilters({ ...filters, diagnostic_type: type, page: 1 });
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Diagnosticos</h1>
<p className="text-sm text-gray-500">Pruebas y analisis de vehiculos</p>
</div>
<Link
to="/diagnostics/new"
className="flex items-center gap-2 rounded-lg bg-diesel-600 px-4 py-2 text-sm font-medium text-white hover:bg-diesel-700"
>
<Plus className="h-4 w-4" />
Nuevo Diagnostico
</Link>
</div>
{/* Type Filters */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{DIAGNOSTIC_TYPES.map((type) => {
const Icon = type.icon;
return (
<button
key={type.value}
onClick={() => handleTypeFilter(filters.diagnostic_type === type.value ? undefined : type.value)}
className={`flex items-center gap-3 rounded-lg border p-4 transition-colors ${
filters.diagnostic_type === type.value
? 'border-diesel-500 bg-diesel-50'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<Icon className={`h-5 w-5 ${filters.diagnostic_type === type.value ? 'text-diesel-600' : 'text-gray-400'}`} />
<span className="text-sm font-medium text-gray-700">{type.label}</span>
</button>
);
})}
</div>
{/* Search & Filters */}
<div className="flex flex-wrap items-center gap-4">
<div className="flex flex-1 items-center gap-2">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Buscar por vehiculo, cliente..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="w-full rounded-lg border border-gray-300 py-2 pl-10 pr-4 text-sm focus:border-diesel-500 focus:outline-none focus:ring-1 focus:ring-diesel-500"
/>
</div>
<button
onClick={handleSearch}
className="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200"
>
Buscar
</button>
</div>
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-gray-400" />
<select
value={filters.result || ''}
onChange={(e) => setFilters({ ...filters, result: e.target.value as DiagnosticResult || undefined, page: 1 })}
className="rounded-lg border border-gray-300 py-2 pl-3 pr-8 text-sm focus:border-diesel-500 focus:outline-none"
>
<option value="">Todos los resultados</option>
{Object.entries(RESULT_CONFIG).map(([key, config]) => (
<option key={key} value={key}>{config.label}</option>
))}
</select>
</div>
</div>
{/* Table */}
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white">
{isLoading ? (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-diesel-600" />
</div>
) : error ? (
<div className="flex h-64 flex-col items-center justify-center text-gray-500">
<AlertCircle className="mb-2 h-8 w-8" />
<p>Error al cargar diagnosticos</p>
</div>
) : diagnostics.length === 0 ? (
<div className="flex h-64 flex-col items-center justify-center text-gray-500">
<Stethoscope className="mb-2 h-8 w-8" />
<p>No hay diagnosticos registrados</p>
<Link
to="/diagnostics/new"
className="mt-2 text-sm text-diesel-600 hover:text-diesel-700"
>
Realizar primer diagnostico
</Link>
</div>
) : (
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Vehiculo
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Tipo
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Resultado
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Orden
</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
Fecha
</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">
Acciones
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{diagnostics.map((diagnostic: Diagnostic) => {
const typeConfig = DIAGNOSTIC_TYPES.find(t => t.value === diagnostic.diagnostic_type);
const TypeIcon = typeConfig?.icon || Stethoscope;
const resultConfig = diagnostic.result ? RESULT_CONFIG[diagnostic.result] : null;
const ResultIcon = resultConfig?.icon;
return (
<tr key={diagnostic.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
<Truck className="h-5 w-5 text-gray-600" />
</div>
<div>
<p className="font-medium text-gray-900">{diagnostic.vehicle_info}</p>
<p className="text-sm text-gray-500">{diagnostic.customer_name}</p>
</div>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4">
<div className="flex items-center gap-2">
<TypeIcon className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-700">{typeConfig?.label || diagnostic.diagnostic_type}</span>
</div>
</td>
<td className="whitespace-nowrap px-6 py-4">
{resultConfig ? (
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${resultConfig.color}`}>
{ResultIcon && <ResultIcon className="h-3 w-3" />}
{resultConfig.label}
</span>
) : (
<span className="text-sm text-gray-400">Pendiente</span>
)}
</td>
<td className="whitespace-nowrap px-6 py-4">
{diagnostic.order_number ? (
<Link
to={`/orders/${diagnostic.order_id}`}
className="text-sm text-diesel-600 hover:text-diesel-700"
>
{diagnostic.order_number}
</Link>
) : (
<span className="text-sm text-gray-400">-</span>
)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
{new Date(diagnostic.performed_at).toLocaleDateString('es-MX', {
day: '2-digit',
month: 'short',
year: 'numeric',
})}
</div>
</td>
<td className="whitespace-nowrap px-6 py-4 text-right">
<Link
to={`/diagnostics/${diagnostic.id}`}
className="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
>
<Eye className="h-4 w-4" />
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
)}
{/* Pagination */}
{data?.data && diagnostics.length > 0 && (
<div className="flex items-center justify-between border-t border-gray-200 bg-gray-50 px-6 py-3">
<div className="text-sm text-gray-500">
Pagina {data.data.page} de {data.data.totalPages}
</div>
<div className="flex gap-2">
<button
onClick={() => setFilters({ ...filters, page: (filters.page || 1) - 1 })}
disabled={(filters.page || 1) <= 1}
className="rounded-lg border border-gray-300 px-3 py-1 text-sm disabled:opacity-50"
>
Anterior
</button>
<button
onClick={() => setFilters({ ...filters, page: (filters.page || 1) + 1 })}
disabled={(filters.page || 1) >= data.data.totalPages}
className="rounded-lg border border-gray-300 px-3 py-1 text-sm disabled:opacity-50"
>
Siguiente
</button>
</div>
</div>
)}
</div>
</div>
);
}