280 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|