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.database import get_core_db
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||||
from .models import ClientOrProviderEnum
|
from .models import ClientOrProviderEnum
|
||||||
@@ -32,10 +33,16 @@ router.include_router(imports_router, prefix="/imports", tags=["clients_and_prov
|
|||||||
@router.get("/", response_model=ClientProviderPaginatedResponseDTO)
|
@router.get("/", response_model=ClientProviderPaginatedResponseDTO)
|
||||||
async def get_clients_and_providers(
|
async def get_clients_and_providers(
|
||||||
company_id: int = Query(..., description="Company ID"),
|
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(
|
type: Optional[ClientOrProviderEnum] = Query(
|
||||||
None, description="Type of entity (client or provider)"
|
None, description="Type of entity (client or provider)"
|
||||||
),
|
),
|
||||||
active: Optional[bool] = Query(None, description="Active status"),
|
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),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
@@ -44,6 +51,9 @@ async def get_clients_and_providers(
|
|||||||
"""Get clients and providers"""
|
"""Get clients and providers"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
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(
|
query = db.query(ClientProvider).options(
|
||||||
joinedload(ClientProvider.address),
|
joinedload(ClientProvider.address),
|
||||||
joinedload(ClientProvider.programs)
|
joinedload(ClientProvider.programs)
|
||||||
@@ -52,9 +62,14 @@ async def get_clients_and_providers(
|
|||||||
ClientProvider.company_id == company_id,
|
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:
|
if type is not None:
|
||||||
# Include 'both' type when filtering by client or provider
|
# Include 'both' type when filtering by client or provider
|
||||||
from sqlalchemy import or_
|
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
ClientProvider.client_or_provider == type,
|
ClientProvider.client_or_provider == type,
|
||||||
@@ -66,13 +81,13 @@ async def get_clients_and_providers(
|
|||||||
query = query.filter(ClientProvider.is_active == active)
|
query = query.filter(ClientProvider.is_active == active)
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
clients = query.offset(skip).limit(limit).all()
|
clients = query.offset(resolved_skip).limit(resolved_limit).all()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"items": [ClientProviderResponseDTO.model_validate(c) for c in clients],
|
"items": [ClientProviderResponseDTO.model_validate(c) for c in clients],
|
||||||
"total": total,
|
"total": total,
|
||||||
"page": (skip // limit) + 1,
|
"page": (resolved_skip // resolved_limit) + 1,
|
||||||
"page_size": limit,
|
"page_size": resolved_limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO)
|
@router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getToken, authStore } from '$lib/auth';
|
import { getToken, authStore } from '$lib/auth';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
const api_url = import.meta.env.VITE_API_URL;
|
const api_url = import.meta.env.VITE_API_URL ?? '';
|
||||||
const BASE_URL = `${api_url.endsWith('/') ? api_url : api_url + '/'}v1/core/help-center`;
|
const normalizedApiUrl = api_url ? (api_url.endsWith('/') ? api_url : `${api_url}/`) : '/';
|
||||||
|
const BASE_URL = `${normalizedApiUrl}v1/core/help-center`;
|
||||||
|
|
||||||
function getAuthToken(): string | null {
|
function getAuthToken(): string | null {
|
||||||
// 1. First try getToken() which checks Keycloak and localStorage
|
// 1. First try getToken() which checks Keycloak and localStorage
|
||||||
|
|||||||
@@ -26,17 +26,18 @@
|
|||||||
let items = $state<ClientProvider[]>(data.items || []);
|
let items = $state<ClientProvider[]>(data.items || []);
|
||||||
let selectedItem = $state<ClientProvider | null>(null);
|
let selectedItem = $state<ClientProvider | null>(null);
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let hasMore = $derived(items.length < totalItems);
|
|
||||||
|
|
||||||
// Server-side filtering/pagination parameters
|
// Server-side filtering/pagination parameters
|
||||||
let currentPage = $state(data.page || 1);
|
let currentPage = $state(data.page || 1);
|
||||||
let pageSize = $state(50);
|
let pageSize = $state(50);
|
||||||
let totalItems = $state(data.total || 0);
|
let totalItems = $state(data.total || 0);
|
||||||
|
let hasMore = $derived(items.length < totalItems);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
let searchName = $state('');
|
let searchName = $state('');
|
||||||
let searchRfc = $state('');
|
let searchRfc = $state('');
|
||||||
let searchType = $state<string>($page.url.searchParams.get('type') || 'both');
|
let searchType = $state<string>($page.url.searchParams.get('type') || 'both');
|
||||||
|
let filterDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// Estado para el diálogo de crear
|
// Estado para el diálogo de crear
|
||||||
let showCreateDialog = $state(false);
|
let showCreateDialog = $state(false);
|
||||||
@@ -75,8 +76,10 @@
|
|||||||
try {
|
try {
|
||||||
const filters: any = {};
|
const filters: any = {};
|
||||||
if (searchType !== 'both') filters.type = searchType;
|
if (searchType !== 'both') filters.type = searchType;
|
||||||
if (searchName) filters.name = searchName;
|
const trimmedName = searchName.trim();
|
||||||
if (searchRfc) filters.rfc = searchRfc;
|
const trimmedRfc = searchRfc.trim();
|
||||||
|
if (trimmedName) filters.name = trimmedName;
|
||||||
|
if (trimmedRfc) filters.rfc = trimmedRfc;
|
||||||
|
|
||||||
const response = await clientsProvidersApi.list(companyId, pageToLoad, pageSize, filters);
|
const response = await clientsProvidersApi.list(companyId, pageToLoad, pageSize, filters);
|
||||||
|
|
||||||
@@ -114,11 +117,6 @@
|
|||||||
|
|
||||||
function handleTypeChange(value: string) {
|
function handleTypeChange(value: string) {
|
||||||
searchType = value;
|
searchType = value;
|
||||||
loadItems(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSearch() {
|
|
||||||
loadItems(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectItem(item: ClientProvider) {
|
function selectItem(item: ClientProvider) {
|
||||||
@@ -162,6 +160,26 @@
|
|||||||
recargar: () => loadItems(1)
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-[calc(100vh-4rem)] p-4 gap-4 pb-15">
|
<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>
|
<h2 class="text-sm font-semibold">Filtros</h2>
|
||||||
<span class="text-xs text-muted-foreground">Busque por nombre, RFC/TAX-ID o tipo</span>
|
<span class="text-xs text-muted-foreground">Busque por nombre, RFC/TAX-ID o tipo</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-xs">Nombre / Razón Social</Label>
|
<Label class="text-xs">Nombre / Razón Social</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={searchName}
|
bind:value={searchName}
|
||||||
placeholder="Buscar por nombre..."
|
placeholder="Buscar por nombre..."
|
||||||
class="h-9"
|
class="h-9"
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -213,7 +230,6 @@
|
|||||||
bind:value={searchRfc}
|
bind:value={searchRfc}
|
||||||
placeholder="RFC / TAX-ID..."
|
placeholder="RFC / TAX-ID..."
|
||||||
class="h-9"
|
class="h-9"
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -233,11 +249,6 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end">
|
|
||||||
<Button variant="secondary" size="sm" class="w-full" onclick={handleSearch}>
|
|
||||||
Buscar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user