feat(cfdi): add autocomplete for emisor and receptor filters

- Add /cfdi/emisores and /cfdi/receptores API endpoints
- Search by RFC or nombre with ILIKE
- Show suggestions dropdown while typing (min 2 chars)
- Click suggestion to select and populate filter input
- Show loading state while searching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-02-17 07:07:01 +00:00
parent 5c6367839f
commit 0e49c0922d
5 changed files with 173 additions and 7 deletions

View File

@@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useCfdis, useCreateCfdi, useDeleteCfdi } from '@/lib/hooks/use-cfdi';
import { createManyCfdis } from '@/lib/api/cfdi';
import { createManyCfdis, searchEmisores, searchReceptores, type EmisorReceptor } from '@/lib/api/cfdi';
import type { CfdiFilters, TipoCfdi, Cfdi } from '@horux/shared';
import type { CreateCfdiData } from '@/lib/api/cfdi';
import { FileText, Search, ChevronLeft, ChevronRight, Plus, Upload, Trash2, X, FileUp, CheckCircle, AlertCircle, Loader2, Eye, Filter, XCircle, Calendar, User, Building2 } from 'lucide-react';
@@ -238,6 +238,9 @@ export default function CfdiPage() {
receptor: '',
});
const [openFilter, setOpenFilter] = useState<'fecha' | 'emisor' | 'receptor' | null>(null);
const [emisorSuggestions, setEmisorSuggestions] = useState<EmisorReceptor[]>([]);
const [receptorSuggestions, setReceptorSuggestions] = useState<EmisorReceptor[]>([]);
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
const [showForm, setShowForm] = useState(false);
const [showBulkForm, setShowBulkForm] = useState(false);
const [formData, setFormData] = useState<CreateCfdiData>(initialFormData);
@@ -288,6 +291,52 @@ export default function CfdiPage() {
setFilters({ ...filters, search: searchTerm, page: 1 });
};
// Debounced search for emisor suggestions
const handleEmisorSearch = useCallback(async (value: string) => {
setColumnFilters(prev => ({ ...prev, emisor: value }));
if (value.length < 2) {
setEmisorSuggestions([]);
return;
}
setLoadingSuggestions(true);
try {
const results = await searchEmisores(value);
setEmisorSuggestions(results);
} catch {
setEmisorSuggestions([]);
} finally {
setLoadingSuggestions(false);
}
}, []);
// Debounced search for receptor suggestions
const handleReceptorSearch = useCallback(async (value: string) => {
setColumnFilters(prev => ({ ...prev, receptor: value }));
if (value.length < 2) {
setReceptorSuggestions([]);
return;
}
setLoadingSuggestions(true);
try {
const results = await searchReceptores(value);
setReceptorSuggestions(results);
} catch {
setReceptorSuggestions([]);
} finally {
setLoadingSuggestions(false);
}
}, []);
const selectEmisor = (emisor: EmisorReceptor) => {
setColumnFilters(prev => ({ ...prev, emisor: emisor.nombre }));
setEmisorSuggestions([]);
};
const selectReceptor = (receptor: EmisorReceptor) => {
setColumnFilters(prev => ({ ...prev, receptor: receptor.nombre }));
setReceptorSuggestions([]);
};
const applyDateFilter = () => {
setFilters({
...filters,
@@ -1227,18 +1276,38 @@ export default function CfdiPage() {
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="start">
<PopoverContent className="w-72" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por emisor</h4>
<div>
<div className="relative">
<Label className="text-xs">RFC o Nombre</Label>
<Input
placeholder="Buscar emisor..."
className="h-8 text-sm"
value={columnFilters.emisor}
onChange={(e) => setColumnFilters({ ...columnFilters, emisor: e.target.value })}
onChange={(e) => handleEmisorSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && applyEmisorFilter()}
/>
{emisorSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border rounded-md shadow-lg max-h-48 overflow-y-auto z-50">
{emisorSuggestions.map((emisor, idx) => (
<button
key={idx}
type="button"
className="w-full px-3 py-2 text-left text-sm hover:bg-muted transition-colors border-b last:border-b-0"
onClick={() => selectEmisor(emisor)}
>
<p className="font-medium truncate">{emisor.nombre}</p>
<p className="text-xs text-muted-foreground">{emisor.rfc}</p>
</button>
))}
</div>
)}
{loadingSuggestions && columnFilters.emisor.length >= 2 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border rounded-md shadow-lg p-2 text-center text-sm text-muted-foreground">
Buscando...
</div>
)}
</div>
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={applyEmisorFilter}>
@@ -1264,18 +1333,38 @@ export default function CfdiPage() {
<Filter className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-64" align="start">
<PopoverContent className="w-72" align="start">
<div className="space-y-3">
<h4 className="font-medium text-sm">Filtrar por receptor</h4>
<div>
<div className="relative">
<Label className="text-xs">RFC o Nombre</Label>
<Input
placeholder="Buscar receptor..."
className="h-8 text-sm"
value={columnFilters.receptor}
onChange={(e) => setColumnFilters({ ...columnFilters, receptor: e.target.value })}
onChange={(e) => handleReceptorSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && applyReceptorFilter()}
/>
{receptorSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border rounded-md shadow-lg max-h-48 overflow-y-auto z-50">
{receptorSuggestions.map((receptor, idx) => (
<button
key={idx}
type="button"
className="w-full px-3 py-2 text-left text-sm hover:bg-muted transition-colors border-b last:border-b-0"
onClick={() => selectReceptor(receptor)}
>
<p className="font-medium truncate">{receptor.nombre}</p>
<p className="text-xs text-muted-foreground">{receptor.rfc}</p>
</button>
))}
</div>
)}
{loadingSuggestions && columnFilters.receptor.length >= 2 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border rounded-md shadow-lg p-2 text-center text-sm text-muted-foreground">
Buscando...
</div>
)}
</div>
<div className="flex gap-2">
<Button size="sm" className="flex-1" onClick={applyReceptorFilter}>