feature/filtros-cliente-proveedor
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user