diff --git a/apps/api/src/controllers/cfdi.controller.ts b/apps/api/src/controllers/cfdi.controller.ts index 2b1f689..53a04e5 100644 --- a/apps/api/src/controllers/cfdi.controller.ts +++ b/apps/api/src/controllers/cfdi.controller.ts @@ -67,6 +67,42 @@ export async function getXml(req: Request, res: Response, next: NextFunction) { } } +export async function getEmisores(req: Request, res: Response, next: NextFunction) { + try { + if (!req.tenantSchema) { + return next(new AppError(400, 'Schema no configurado')); + } + + const search = (req.query.search as string) || ''; + if (search.length < 2) { + return res.json([]); + } + + const emisores = await cfdiService.getEmisores(req.tenantSchema, search); + res.json(emisores); + } catch (error) { + next(error); + } +} + +export async function getReceptores(req: Request, res: Response, next: NextFunction) { + try { + if (!req.tenantSchema) { + return next(new AppError(400, 'Schema no configurado')); + } + + const search = (req.query.search as string) || ''; + if (search.length < 2) { + return res.json([]); + } + + const receptores = await cfdiService.getReceptores(req.tenantSchema, search); + res.json(receptores); + } catch (error) { + next(error); + } +} + export async function getResumen(req: Request, res: Response, next: NextFunction) { try { if (!req.tenantSchema) { diff --git a/apps/api/src/routes/cfdi.routes.ts b/apps/api/src/routes/cfdi.routes.ts index 703f0e7..988b1b6 100644 --- a/apps/api/src/routes/cfdi.routes.ts +++ b/apps/api/src/routes/cfdi.routes.ts @@ -10,6 +10,8 @@ router.use(tenantMiddleware); router.get('/', cfdiController.getCfdis); router.get('/resumen', cfdiController.getResumen); +router.get('/emisores', cfdiController.getEmisores); +router.get('/receptores', cfdiController.getReceptores); router.get('/:id', cfdiController.getCfdiById); router.get('/:id/xml', cfdiController.getXml); router.post('/', cfdiController.createCfdi); diff --git a/apps/api/src/services/cfdi.service.ts b/apps/api/src/services/cfdi.service.ts index 2f59d3e..f4219bc 100644 --- a/apps/api/src/services/cfdi.service.ts +++ b/apps/api/src/services/cfdi.service.ts @@ -387,6 +387,28 @@ export async function deleteCfdi(schema: string, id: string): Promise { await prisma.$queryRawUnsafe(`DELETE FROM "${schema}".cfdis WHERE id = $1`, id); } +export async function getEmisores(schema: string, search: string, limit: number = 10): Promise<{ rfc: string; nombre: string }[]> { + const result = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string }[]>(` + SELECT DISTINCT rfc_emisor as rfc, nombre_emisor as nombre + FROM "${schema}".cfdis + WHERE rfc_emisor ILIKE $1 OR nombre_emisor ILIKE $1 + ORDER BY nombre_emisor + LIMIT $2 + `, `%${search}%`, limit); + return result; +} + +export async function getReceptores(schema: string, search: string, limit: number = 10): Promise<{ rfc: string; nombre: string }[]> { + const result = await prisma.$queryRawUnsafe<{ rfc: string; nombre: string }[]>(` + SELECT DISTINCT rfc_receptor as rfc, nombre_receptor as nombre + FROM "${schema}".cfdis + WHERE rfc_receptor ILIKE $1 OR nombre_receptor ILIKE $1 + ORDER BY nombre_receptor + LIMIT $2 + `, `%${search}%`, limit); + return result; +} + export async function getResumenCfdis(schema: string, año: number, mes: number) { const result = await prisma.$queryRawUnsafe<[{ total_ingresos: number; diff --git a/apps/web/app/(dashboard)/cfdi/page.tsx b/apps/web/app/(dashboard)/cfdi/page.tsx index 2e92278..aa60004 100644 --- a/apps/web/app/(dashboard)/cfdi/page.tsx +++ b/apps/web/app/(dashboard)/cfdi/page.tsx @@ -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([]); + const [receptorSuggestions, setReceptorSuggestions] = useState([]); + const [loadingSuggestions, setLoadingSuggestions] = useState(false); const [showForm, setShowForm] = useState(false); const [showBulkForm, setShowBulkForm] = useState(false); const [formData, setFormData] = useState(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() { - +

Filtrar por emisor

-
+
setColumnFilters({ ...columnFilters, emisor: e.target.value })} + onChange={(e) => handleEmisorSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && applyEmisorFilter()} /> + {emisorSuggestions.length > 0 && ( +
+ {emisorSuggestions.map((emisor, idx) => ( + + ))} +
+ )} + {loadingSuggestions && columnFilters.emisor.length >= 2 && ( +
+ Buscando... +
+ )}
- +

Filtrar por receptor

-
+
setColumnFilters({ ...columnFilters, receptor: e.target.value })} + onChange={(e) => handleReceptorSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && applyReceptorFilter()} /> + {receptorSuggestions.length > 0 && ( +
+ {receptorSuggestions.map((receptor, idx) => ( + + ))} +
+ )} + {loadingSuggestions && columnFilters.receptor.length >= 2 && ( +
+ Buscando... +
+ )}