diff --git a/apps/api/src/services/cfdi.service.ts b/apps/api/src/services/cfdi.service.ts index f4219bc..f7eae11 100644 --- a/apps/api/src/services/cfdi.service.ts +++ b/apps/api/src/services/cfdi.service.ts @@ -50,14 +50,9 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise(` - SELECT COUNT(*) as count FROM "${schema}".cfdis ${whereClause} - `, ...params); - - const total = Number(countResult[0]?.count || 0); - + // Combinar COUNT con la query principal usando window function params.push(limit, offset); - const data = await prisma.$queryRawUnsafe(` + const dataWithCount = await prisma.$queryRawUnsafe<(Cfdi & { total_count: number })[]>(` SELECT id, uuid_fiscal as "uuidFiscal", tipo, serie, folio, fecha_emision as "fechaEmision", fecha_timbrado as "fechaTimbrado", @@ -68,13 +63,17 @@ export async function getCfdis(schema: string, filters: CfdiFilters): Promise cfdi) as Cfdi[]; + return { data, total, diff --git a/apps/web/app/(dashboard)/cfdi/page.tsx b/apps/web/app/(dashboard)/cfdi/page.tsx index aa60004..bc0ea39 100644 --- a/apps/web/app/(dashboard)/cfdi/page.tsx +++ b/apps/web/app/(dashboard)/cfdi/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState, useRef, useCallback } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; +import { useDebounce } from '@/lib/hooks/use-debounce'; import { Header } from '@/components/layouts/header'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -240,8 +241,39 @@ export default function CfdiPage() { const [openFilter, setOpenFilter] = useState<'fecha' | 'emisor' | 'receptor' | null>(null); const [emisorSuggestions, setEmisorSuggestions] = useState([]); const [receptorSuggestions, setReceptorSuggestions] = useState([]); - const [loadingSuggestions, setLoadingSuggestions] = useState(false); + const [loadingEmisor, setLoadingEmisor] = useState(false); + const [loadingReceptor, setLoadingReceptor] = useState(false); const [showForm, setShowForm] = useState(false); + + // Debounced values for autocomplete + const debouncedEmisor = useDebounce(columnFilters.emisor, 300); + const debouncedReceptor = useDebounce(columnFilters.receptor, 300); + + // Fetch emisor suggestions when debounced value changes + useEffect(() => { + if (debouncedEmisor.length < 2) { + setEmisorSuggestions([]); + return; + } + setLoadingEmisor(true); + searchEmisores(debouncedEmisor) + .then(setEmisorSuggestions) + .catch(() => setEmisorSuggestions([])) + .finally(() => setLoadingEmisor(false)); + }, [debouncedEmisor]); + + // Fetch receptor suggestions when debounced value changes + useEffect(() => { + if (debouncedReceptor.length < 2) { + setReceptorSuggestions([]); + return; + } + setLoadingReceptor(true); + searchReceptores(debouncedReceptor) + .then(setReceptorSuggestions) + .catch(() => setReceptorSuggestions([])) + .finally(() => setLoadingReceptor(false)); + }, [debouncedReceptor]); const [showBulkForm, setShowBulkForm] = useState(false); const [formData, setFormData] = useState(initialFormData); const [bulkData, setBulkData] = useState(''); @@ -291,42 +323,6 @@ 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([]); @@ -1285,7 +1281,7 @@ export default function CfdiPage() { placeholder="Buscar emisor..." className="h-8 text-sm" value={columnFilters.emisor} - onChange={(e) => handleEmisorSearch(e.target.value)} + onChange={(e) => setColumnFilters(prev => ({ ...prev, emisor: e.target.value }))} onKeyDown={(e) => e.key === 'Enter' && applyEmisorFilter()} /> {emisorSuggestions.length > 0 && ( @@ -1303,7 +1299,7 @@ export default function CfdiPage() { ))} )} - {loadingSuggestions && columnFilters.emisor.length >= 2 && ( + {loadingEmisor && columnFilters.emisor.length >= 2 && emisorSuggestions.length === 0 && (
Buscando...
@@ -1342,7 +1338,7 @@ export default function CfdiPage() { placeholder="Buscar receptor..." className="h-8 text-sm" value={columnFilters.receptor} - onChange={(e) => handleReceptorSearch(e.target.value)} + onChange={(e) => setColumnFilters(prev => ({ ...prev, receptor: e.target.value }))} onKeyDown={(e) => e.key === 'Enter' && applyReceptorFilter()} /> {receptorSuggestions.length > 0 && ( @@ -1360,7 +1356,7 @@ export default function CfdiPage() { ))} )} - {loadingSuggestions && columnFilters.receptor.length >= 2 && ( + {loadingReceptor && columnFilters.receptor.length >= 2 && receptorSuggestions.length === 0 && (
Buscando...
diff --git a/apps/web/lib/hooks/use-cfdi.ts b/apps/web/lib/hooks/use-cfdi.ts index 05596c1..dd669b5 100644 --- a/apps/web/lib/hooks/use-cfdi.ts +++ b/apps/web/lib/hooks/use-cfdi.ts @@ -7,6 +7,8 @@ export function useCfdis(filters: CfdiFilters) { return useQuery({ queryKey: ['cfdis', filters], queryFn: () => cfdiApi.getCfdis(filters), + staleTime: 30 * 1000, // 30 segundos + gcTime: 5 * 60 * 1000, // 5 minutos en cache }); } diff --git a/apps/web/lib/hooks/use-debounce.ts b/apps/web/lib/hooks/use-debounce.ts new file mode 100644 index 0000000..e3706e6 --- /dev/null +++ b/apps/web/lib/hooks/use-debounce.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}