feature/filtros-cliente-proveedor

This commit is contained in:
2026-04-24 08:33:27 -06:00
parent db18b4463d
commit 9bdb401e78
3 changed files with 49 additions and 22 deletions

View File

@@ -7,6 +7,7 @@ from typing import List, Optional
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import or_
from sqlalchemy.orm import Session, joinedload
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import ClientOrProviderEnum
@@ -32,10 +33,16 @@ router.include_router(imports_router, prefix="/imports", tags=["clients_and_prov
@router.get("/", response_model=ClientProviderPaginatedResponseDTO)
async def get_clients_and_providers(
company_id: int = Query(..., description="Company ID"),
name: Optional[str] = Query(None, description="Filter by name (contains)"),
rfc: Optional[str] = Query(None, description="Filter by RFC/TAX-ID (contains)"),
type: Optional[ClientOrProviderEnum] = Query(
None, description="Type of entity (client or provider)"
),
active: Optional[bool] = Query(None, description="Active status"),
page: Optional[int] = Query(None, ge=1, description="Page number (1-based)"),
page_size: Optional[int] = Query(
None, ge=1, le=1000, description="Page size when using page-based pagination"
),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
@@ -44,6 +51,9 @@ async def get_clients_and_providers(
"""Get clients and providers"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
resolved_limit = page_size if page_size is not None else limit
resolved_skip = ((page - 1) * resolved_limit) if page is not None else skip
query = db.query(ClientProvider).options(
joinedload(ClientProvider.address),
joinedload(ClientProvider.programs)
@@ -52,9 +62,14 @@ async def get_clients_and_providers(
ClientProvider.company_id == company_id,
)
if name:
query = query.filter(ClientProvider.name.ilike(f"%{name.strip()}%"))
if rfc:
query = query.filter(ClientProvider.rfc.ilike(f"%{rfc.strip()}%"))
if type is not None:
# Include 'both' type when filtering by client or provider
from sqlalchemy import or_
query = query.filter(
or_(
ClientProvider.client_or_provider == type,
@@ -66,13 +81,13 @@ async def get_clients_and_providers(
query = query.filter(ClientProvider.is_active == active)
total = query.count()
clients = query.offset(skip).limit(limit).all()
clients = query.offset(resolved_skip).limit(resolved_limit).all()
return {
"items": [ClientProviderResponseDTO.model_validate(c) for c in clients],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
"page": (resolved_skip // resolved_limit) + 1,
"page_size": resolved_limit,
}
@router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO)

View File

@@ -1,8 +1,9 @@
import { getToken, authStore } from '$lib/auth';
import { get } from 'svelte/store';
const api_url = import.meta.env.VITE_API_URL;
const BASE_URL = `${api_url.endsWith('/') ? api_url : api_url + '/'}v1/core/help-center`;
const api_url = import.meta.env.VITE_API_URL ?? '';
const normalizedApiUrl = api_url ? (api_url.endsWith('/') ? api_url : `${api_url}/`) : '/';
const BASE_URL = `${normalizedApiUrl}v1/core/help-center`;
function getAuthToken(): string | null {
// 1. First try getToken() which checks Keycloak and localStorage

View File

@@ -26,17 +26,18 @@
let items = $state<ClientProvider[]>(data.items || []);
let selectedItem = $state<ClientProvider | null>(null);
let isLoading = $state(false);
let hasMore = $derived(items.length < totalItems);
// Server-side filtering/pagination parameters
let currentPage = $state(data.page || 1);
let pageSize = $state(50);
let totalItems = $state(data.total || 0);
let hasMore = $derived(items.length < totalItems);
// Filter state
let searchName = $state('');
let searchRfc = $state('');
let searchType = $state<string>($page.url.searchParams.get('type') || 'both');
let filterDebounce: ReturnType<typeof setTimeout> | null = null;
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
@@ -75,8 +76,10 @@
try {
const filters: any = {};
if (searchType !== 'both') filters.type = searchType;
if (searchName) filters.name = searchName;
if (searchRfc) filters.rfc = searchRfc;
const trimmedName = searchName.trim();
const trimmedRfc = searchRfc.trim();
if (trimmedName) filters.name = trimmedName;
if (trimmedRfc) filters.rfc = trimmedRfc;
const response = await clientsProvidersApi.list(companyId, pageToLoad, pageSize, filters);
@@ -114,11 +117,6 @@
function handleTypeChange(value: string) {
searchType = value;
loadItems(1);
}
function handleSearch() {
loadItems(1);
}
function selectItem(item: ClientProvider) {
@@ -162,6 +160,26 @@
recargar: () => loadItems(1)
})
);
$effect(() => {
const companyId = companyStore.activeCompany?.id;
if (!browser || !companyId) return;
searchName;
searchRfc;
searchType;
pageSize;
if (filterDebounce) clearTimeout(filterDebounce);
filterDebounce = setTimeout(() => {
selectedItem = null;
loadItems(1);
}, 350);
return () => {
if (filterDebounce) clearTimeout(filterDebounce);
};
});
</script>
<div class="flex flex-col h-[calc(100vh-4rem)] p-4 gap-4 pb-15">
@@ -197,14 +215,13 @@
<h2 class="text-sm font-semibold">Filtros</h2>
<span class="text-xs text-muted-foreground">Busque por nombre, RFC/TAX-ID o tipo</span>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label class="text-xs">Nombre / Razón Social</Label>
<Input
bind:value={searchName}
placeholder="Buscar por nombre..."
class="h-9"
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<div class="space-y-2">
@@ -213,7 +230,6 @@
bind:value={searchRfc}
placeholder="RFC / TAX-ID..."
class="h-9"
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<div class="space-y-2">
@@ -233,11 +249,6 @@
</Select.Content>
</Select.Root>
</div>
<div class="flex items-end">
<Button variant="secondary" size="sm" class="w-full" onclick={handleSearch}>
Buscar
</Button>
</div>
</div>
</div>
</div>