diff --git a/.env.example b/.env.example index 1ef60c19..4f0f10d8 100644 --- a/.env.example +++ b/.env.example @@ -61,6 +61,9 @@ S3_USE_SSL=false S3_FILE_STORAGE=true S3_PRESIGNED_EXPIRES_SECONDS=3600 +COVE_FIEL_HASH_KEY= +COVE_FIEL_HASH_IV= + # ----- Sitar API ----- SITAR_API_URL=http://api.sitar.aduanasoft.com SITAR_API_USER=user_sitar_api diff --git a/backend/.env.example b/backend/.env.example index 77ae339c..bfaae05d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -28,6 +28,9 @@ CORS_ORIGINS=http://localhost:5173,http://localhost:3000 # License Service LICENSE_CHECK_ENABLED=True +COVE_FIEL_HASH_KEY= +COVE_FIEL_HASH_IV= + # Synchronization (Hub & Spoke) SYNC_SECRET_TOKEN=change-this-sync-token-in-production # Only for spokes/clients. Leave empty if this is the Hub. diff --git a/backend/api/v1/modules/a76/factura_cove/external_service.py b/backend/api/v1/modules/a76/factura_cove/external_service.py new file mode 100644 index 00000000..ee8af99c --- /dev/null +++ b/backend/api/v1/modules/a76/factura_cove/external_service.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +import logging + +import httpx + +from core.config import settings +from .schemas import FacturaCoveRequest + + +logger = logging.getLogger(__name__) + + +@dataclass +class CoveExternalResult: + """ + Resultado simplificado de la llamada al servicio externo de COVE. + + Por ahora usamos un stub que simula una respuesta exitosa y devuelve + un número de COVE ficticio para poder probar el flujo end-to-end + (task_id + cove_number) sin depender del ambiente externo. + """ + + status: str + message: str | None = None + cove_number: str | None = None + vucem_operation_num: str | None = None + raw_response: Dict[str, Any] | None = None + + +class CoveExternalService: + """ + Cliente del API externo de COVE. + + NOTA IMPORTANTE: + ---------------- + Esta implementación es, por ahora, un stub que: + - No realiza la llamada HTTP real. + - Genera un número de COVE ficticio basado en los datos de la factura. + + Cuando se tenga disponible la URL y contrato exacto del servicio COVE, + este stub se puede reemplazar por una implementación con httpx/requests + que: + - Serialice el FacturaCoveRequest al JSON requerido. + - Realice la petición HTTP. + - Mapee la respuesta real a CoveExternalResult. + """ + + def __init__(self) -> None: + """ + Inicializa el cliente usando COVE_API_URL si está definido; de lo contrario, + usa por defecto el endpoint público documentado en: + https://api.vu.aduanasoft.com/docs#/Factura%20COVE/generar_factura_cove_endpoint_api_v1_factura_cove_generar_factura_cove_post + """ + self.base_url = (settings.COVE_API_URL or "").strip() or "https://api.vu.aduanasoft.com" + + def generate_cove(self, payload: FacturaCoveRequest) -> CoveExternalResult: + """ + Llama al endpoint externo /api/v1/factura-cove/generar-factura-cove + con el FacturaCoveRequest completo y retorna el resultado tal como + lo reporta el servicio remoto. + """ + url = f"{self.base_url.rstrip('/')}/api/v1/factura-cove/generar-factura-cove" + json_payload = payload.model_dump(mode="json") + configuracion_vu = json_payload.get("configuracion_vu") or {} + logger.info( + "Sending COVE payload: invoice=%s rfc_vu=%s clave_fiel_len=%s clave_ws_len=%s cer_len=%s key_len=%s", + json_payload.get("numero_factura"), + configuracion_vu.get("rfc_usuario_vu"), + len(configuracion_vu.get("clave_fiel") or ""), + len(configuracion_vu.get("clave_webservice") or ""), + len(configuracion_vu.get("archivo_cer_base64") or ""), + len(configuracion_vu.get("archivo_key_base64") or ""), + ) + + # NOTA: verify=False desactiva la validación de certificado SSL. + # Esto es útil en entornos de desarrollo o cuando el entorno no confía + # en el certificado del endpoint externo. En producción, idealmente + # se debería habilitar la verificación SSL. + with httpx.Client(timeout=30.0, verify=False) as client: + resp = client.post(url, json=json_payload) + + # Intentar parsear JSON siempre, incluso en errores 4xx/5xx + try: + data: Dict[str, Any] = resp.json() + except Exception: + data = {} + + # Si el servicio externo respondió con error (por ejemplo 422 Validation Error), + # devolvemos un resultado de error rico en información para que la UI pueda + # mostrar el detalle completo. + if resp.status_code >= 400: + # Intentar construir un mensaje amigable + message = None + if isinstance(data, dict): + message = data.get("message") + if not message and "detail" in data: + # FastAPI ValidationError-style: detail: [{loc, msg, type}, ...] + try: + parts = [str(d.get("msg")) for d in data["detail"] if isinstance(d, dict)] + message = "; ".join([p for p in parts if p]) + except Exception: + pass + if not message: + message = resp.text or f"HTTP {resp.status_code}" + + status = "validation_error" if resp.status_code == 422 else "error" + + return CoveExternalResult( + status=status, + message=message, + cove_number=None, + vucem_operation_num=None, + raw_response={ + "status_code": resp.status_code, + "body": data, + }, + ) + + # 2xx: según la especificación del servicio externo, al menos devuelve: + # { "task_id": "...", "status": "...", "message": "..." } + raw_status = str(data.get("status") or "queued") + message = data.get("message") + external_task_id = data.get("task_id") + + # Caso especial: algunos ambientes de VU regresan status="error" pero un mensaje + # tipo "Factura COVE iniciada para: ... Use el task_id para consultar el estado." + # que en realidad indica que la factura fue aceptada y quedó encolada en VU. + # En ese caso NO lo tratamos como error de negocio, sino como "en cola". + normalized_status = raw_status.lower() + if ( + normalized_status == "error" + and isinstance(message, str) + and "Factura COVE iniciada para" in message + ): + status = "external_queued" + else: + status = raw_status + + # El número de COVE normalmente se obtendrá vía /status/{task_id}; por ahora + # lo dejamos en None y exponemos la respuesta completa para inspección en UI. + return CoveExternalResult( + status=status, + message=message, + cove_number=None, + vucem_operation_num=None, + raw_response={ + "task_id": external_task_id, + "status": status, + "message": message, + "raw": data, + }, + ) + + def get_status(self, task_id: str) -> Dict[str, Any]: + """ + Consulta el endpoint externo /api/v1/factura-cove/status/{task_id} + y devuelve el JSON de progreso/resultado tal cual lo envía el servicio. + + Ejemplo de respuesta esperada (simplificada): + { + "task_id": "...", + "state": "PROGRESS" | "SUCCESS" | "FAILURE", + "result": null | {...}, + "error": null | "...", + "progress": { + "current_step": "Consultando respuesta COVE con número de operación", + "progress": 10.5, + "total_steps": 12, + "task_id": "...", + "numero_operacion": "306658625" + } + } + """ + url = f"{self.base_url.rstrip('/')}/api/v1/factura-cove/status/{task_id}" + + # Igual que en generate_cove, desactivamos verify solo para entornos de dev. + with httpx.Client(timeout=30.0, verify=False) as client: + resp = client.get(url) + + try: + data: Dict[str, Any] = resp.json() + except Exception: + data = {} + + # Adjuntar metadatos mínimos de respuesta HTTP + data.setdefault("status_code", resp.status_code) + + return data + diff --git a/backend/api/v1/modules/a76/factura_cove/routes.py b/backend/api/v1/modules/a76/factura_cove/routes.py new file mode 100644 index 00000000..9a311de6 --- /dev/null +++ b/backend/api/v1/modules/a76/factura_cove/routes.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, Dict + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from core.celery_app import celery_app +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token, validate_access_to_resource + +from api.v1.modules.core.tasks_tracking import track_and_dispatch + +from .schemas import ( + CoveEligibilityResponse, + FacturaCoveResponse, + GenerateCoveFromInvoiceRequest, +) +from .service import FacturaCoveDomainService +from .tasks import factura_cove_generate + +router = APIRouter() + + +@router.post( + "/invoices/{invoice_id}/cove", + response_model=FacturaCoveResponse, + summary="Generar COVE a partir de una factura (asíncrono)", +) +def trigger_cove_for_invoice( + invoice_id: int, + body: GenerateCoveFromInvoiceRequest, + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """ + Dispara la tarea Celery `factura_cove_generate` para una factura específica. + + - Valida acceso a la compañía. + - Registra la tarea en el tracker de tareas. + - Retorna el `task_id` para hacer polling de estado desde el frontend. + """ + tenant_id = validate_access_to_resource(db, body.company_id, current_user) + + # Validación mínima de existencia/propiedad de la factura (el dominio hará validaciones más profundas). + from api.v1.modules.a76.invoices.models import InvoiceHeader + + invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id) + if not invoice: + raise HTTPException(status_code=404, detail=f"Factura {invoice_id} no encontrada.") + if invoice.company_id != body.company_id or invoice.tenant_id != tenant_id: + raise HTTPException( + status_code=400, + detail="La factura no pertenece a la compañía o tenant actuales.", + ) + + task = track_and_dispatch( + db=db, + task=factura_cove_generate, + tenant_id=tenant_id, + company_id=body.company_id, + requested_by_user=( + current_user.get("email") + or current_user.get("preferred_username") + or current_user.get("username") + or "system" + ), + task_name="factura_cove_generate", + task_group="factura_cove", + task_origin="a76/factura_cove/invoices/cove", + args=[invoice_id, int(tenant_id), body.company_id, body.recipient_email], + ) + + return FacturaCoveResponse( + task_id=task.id, + status="queued", + message="Tarea de validación/generación de COVE encolada.", + ) + + +@router.get( + "/invoices/cove/{task_id}/status", + summary="Estado de tarea de COVE para factura", +) +def get_cove_status(task_id: str) -> Dict[str, Any]: + """ + Consulta el estado de una tarea Celery de generación de COVE. + + Retorna: + - state: 'PROCESSING' | 'SUCCESS' | 'FAILURE' + - info: { current: int, status: str } (cuando state == 'PROCESSING') + - result: dict (cuando state == 'SUCCESS' o 'FAILURE') + """ + task_result = celery_app.AsyncResult(task_id) + + if task_result.state in ("PENDING", "STARTED"): + return { + "state": "PROCESSING", + "info": {"current": 0, "status": "Iniciando generación de COVE..."}, + } + + if task_result.state == "PROGRESS": + return { + "state": "PROCESSING", + "info": task_result.info or {"current": 0, "status": "Procesando COVE..."}, + } + + if task_result.state == "SUCCESS": + return { + "state": "SUCCESS", + "result": task_result.result, + } + + error_info = task_result.result + if isinstance(error_info, Exception): + error_msg = str(error_info) + else: + error_msg = str(error_info) if error_info else "Error desconocido" + + return { + "state": "FAILURE", + "result": error_msg, + } + + +@router.get( + "/invoices/{invoice_id}/cove/eligibility", + response_model=CoveEligibilityResponse, + summary="Verifica si una factura puede generar COVE", +) +def check_cove_eligibility( + invoice_id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """ + Evalúa si la factura tiene todos los datos necesarios (VU, factura, partidas) + para poder generar un COVE. No dispara la tarea Celery. + """ + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + tenant_id_int = int(tenant_id) + + service = FacturaCoveDomainService(db) + eligibility = service.check_eligibility(invoice_id=invoice_id, tenant_id=tenant_id_int, company_id=company_id) + return eligibility + diff --git a/backend/api/v1/modules/a76/factura_cove/schemas.py b/backend/api/v1/modules/a76/factura_cove/schemas.py new file mode 100644 index 00000000..d9365630 --- /dev/null +++ b/backend/api/v1/modules/a76/factura_cove/schemas.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from datetime import datetime +from decimal import Decimal +from typing import List, Optional, Dict, Any + +from pydantic import BaseModel, EmailStr, Field + + +class ConfiguracionVU(BaseModel): + """ + Configuración de Ventanilla Única / VUCEM para generación de COVE. + + Nota: estos campos se pueden poblar desde CustomsBrokerVU (web_service_user, + web_service_access_key, fiel_access_key, query_tax_id, etc.) o desde el + propio request de la API pública, según el flujo que se implemente. + """ + + rfc_usuario_vu: str = Field(..., max_length=30) + # Clave encriptada/token del webservice; puede ser larga (base64) + clave_webservice: str = Field(..., max_length=512) + archivo_cer_base64: str + archivo_key_base64: str + clave_fiel: str = Field(..., max_length=100) + + +class PersonaCove(BaseModel): + tipo_identificador: str = Field(..., max_length=10) + identificacion: str = Field(..., max_length=30) + apellido_paterno: Optional[str] = Field(None, max_length=80) + apellido_materno: Optional[str] = Field(None, max_length=80) + nombre: Optional[str] = Field(None, max_length=80) + calle: Optional[str] = Field(None, max_length=120) + numero_exterior: Optional[str] = Field(None, max_length=20) + numero_interior: Optional[str] = Field(None, max_length=20) + colonia: Optional[str] = Field(None, max_length=120) + localidad: Optional[str] = Field(None, max_length=120) + municipio: Optional[str] = Field(None, max_length=120) + entidad_federativa: Optional[str] = Field(None, max_length=120) + pais: str = Field(..., max_length=3, description="País en formato ISO o catálogo VU") + codigo_postal: Optional[str] = Field(None, max_length=15) + + +class DescripcionEspecifica(BaseModel): + marca: Optional[str] = Field(None, max_length=80) + modelo: Optional[str] = Field(None, max_length=80) + submodelo: Optional[str] = Field(None, max_length=80) + numero_serie: Optional[str] = Field(None, max_length=80) + + +class MercanciaCove(BaseModel): + descripcion_generica: str = Field(..., max_length=500) + clave_unidad_medida: str = Field(..., max_length=10) + tipo_moneda: str = Field(..., max_length=5) + cantidad: Decimal = Field(..., gt=0) + valor_unitario: Decimal = Field(..., ge=0) + valor_total: Decimal = Field(..., ge=0) + valor_dolares: Optional[Decimal] = Field(None, ge=0) + descripcion_especifica: List[DescripcionEspecifica] = Field(default_factory=list) + + +class FacturaCoveRequest(BaseModel): + """ + Payload completo para generación de COVE. + + Este modelo replica el contrato del servicio externo de COVE que se + mostró en la documentación compartida por el usuario. + """ + + configuracion_vu: ConfiguracionVU + rfc_consulta: str = Field(..., max_length=30) + tipo_figura: str = Field(..., max_length=10) + numero_factura: str = Field(..., max_length=50) + tipo_operacion: str = Field(..., max_length=10) + patente_aduanal: str = Field(..., max_length=10) + fecha_expedicion: datetime + observaciones: Optional[str] = Field(None, max_length=500) + correo_electronico: Optional[EmailStr] = None + tiene_subdivision: bool = False + certificado_origen: bool = False + numero_exportador_autorizado: Optional[str] = Field(None, max_length=50) + emisor: PersonaCove + destinatario: PersonaCove + mercancias: List[MercanciaCove] = Field(default_factory=list, min_length=1) + + +class FacturaCoveResponse(BaseModel): + """ + Respuesta base del endpoint público de generación de COVE. + Para el flujo asíncrono interno, solo usamos task_id/status/message. + """ + + task_id: Optional[str] = None + status: str + message: Optional[str] = None + + +class GenerateCoveFromInvoiceRequest(BaseModel): + """ + Request minimalista desde la vista de facturas. + + Solo necesita el company_id porque el invoice_id viene en la URL y el + tenant_id se resuelve desde el token. + """ + + company_id: int + force_regen: Optional[bool] = False + recipient_email: Optional[EmailStr] = None + + +class GenerateCoveResult(BaseModel): + """ + Resultado estándar que produce la tarea Celery factura_cove_generate. + """ + + status: str + message: Optional[str] = None + invoice_id: Optional[int] = None + cove_number: Optional[str] = None + vucem_operation_num: Optional[str] = None + # ID de tarea devuelto por el servicio externo de COVE (si aplica) + external_task_id: Optional[str] = None + # Respuesta cruda devuelta por el servicio externo (POST generar-factura-cove) + external_response: Optional[Dict[str, Any]] = None + errors: Optional[list[dict]] = None + + +class CoveEligibilityIssue(BaseModel): + field: str + message: str + + +class CoveEligibilityResponse(BaseModel): + can_generate: bool + reasons: list[CoveEligibilityIssue] = Field(default_factory=list) + diff --git a/backend/api/v1/modules/a76/factura_cove/service.py b/backend/api/v1/modules/a76/factura_cove/service.py new file mode 100644 index 00000000..affe7adc --- /dev/null +++ b/backend/api/v1/modules/a76/factura_cove/service.py @@ -0,0 +1,785 @@ +from __future__ import annotations + +import base64 +from dataclasses import dataclass +from decimal import Decimal +from typing import List, Tuple + +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from sqlalchemy.orm import Session + +from core.config import settings +from core.database import CoreSessionLocal +from core.exceptions import ValidationException, ErrorCollector +from core.storage_s3 import get_object_bytes, object_exists + +from api.v1.modules.a76.invoices.models import InvoiceHeader +from api.v1.modules.a76.customs_brokers import models as cb_models +from api.v1.modules.a76.clients_and_providers.models import ClientProvider +from api.v1.modules.a76.general_catalogs.company.models import Company +from api.v1.modules.a76.items.models import LineItem + +from .schemas import ( + ConfiguracionVU, + CoveEligibilityIssue, + CoveEligibilityResponse, + FacturaCoveRequest, + MercanciaCove, + PersonaCove, +) + +@dataclass +class InvoiceContext: + invoice: InvoiceHeader + broker: cb_models.CustomsBroker | None + vu: cb_models.CustomsBrokerVU | None + + +class FacturaCoveDomainService: + """ + Servicio de dominio para validar y construir el payload de COVE a partir de una factura. + + NOTA IMPORTANTE: + ---------------- + Este servicio prepara la estructura de datos y realiza validaciones de negocio, + pero **no** realiza todavía la llamada HTTP al webservice de COVE. Eso se puede + implementar posteriormente en un servicio dedicado (p. ej. CoveExternalService). + """ + + def __init__(self, db: Session): + self.db = db + + def _load_context(self, invoice_id: int, tenant_id: int, company_id: int) -> InvoiceContext: + invoice: InvoiceHeader | None = self.db.get(InvoiceHeader, invoice_id) + if not invoice: + raise ValidationException( + "Factura no encontrada", + errors=[{"field": "invoice_id", "message": f"Factura {invoice_id} no encontrada"}], + ) + + if invoice.company_id != company_id or invoice.tenant_id != tenant_id: + raise ValidationException( + "Factura no pertenece a la compañía/tenant actual", + errors=[ + { + "field": "invoice_id", + "message": "La factura no pertenece a la compañía o tenant actuales", + } + ], + ) + + compliance = invoice.compliance_mx + broker = None + vu = None + + if compliance and compliance.customs_broker_id: + broker = self.db.get(cb_models.CustomsBroker, compliance.customs_broker_id) + if broker: + vu = broker.vu + + return InvoiceContext(invoice=invoice, broker=broker, vu=vu) + + def _get_company(self, ctx: InvoiceContext) -> Company | None: + company_id = getattr(ctx.invoice, "company_id", None) + if not company_id: + return None + return self.db.get(Company, company_id) + + def _get_company_fiel_certificate(self, company: Company | None): + if not company: + return None + + for certificate in company.digital_certificates or []: + if (certificate.certificate_type or "").strip().lower() == "fiel": + return certificate + + return None + + def _encrypt_fiel(self, raw_fiel: str) -> str: + """ + Cifra la clave FIEL con el mismo esquema del sistema legado PHP: + AES-256-CBC + PKCS7 + base64. + """ + normalized_fiel = (raw_fiel or "").strip() + if not normalized_fiel: + return "" + + encryption_key = (settings.COVE_FIEL_HASH_KEY or "").encode("utf-8") + encryption_iv = (settings.COVE_FIEL_HASH_IV or "").encode("utf-8") + if not encryption_key or not encryption_iv: + return "" + + key_bytes = encryption_key[:32].ljust(32, b"\0") + iv_bytes = encryption_iv[:16].ljust(16, b"\0") + + padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_data = padder.update(normalized_fiel.encode("utf-8")) + padder.finalize() + + cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv_bytes)) + encryptor = cipher.encryptor() + encrypted = encryptor.update(padded_data) + encryptor.finalize() + return base64.b64encode(encrypted).decode("ascii") + + def _build_configuracion_vu(self, ctx: InvoiceContext, errors: ErrorCollector) -> ConfiguracionVU | None: + """ + Construye la sección configuracion_vu usando CustomsBrokerVU + S3. + + Lee los archivos .cer y .key desde almacenamiento de objetos, los + convierte a base64 y construye un ConfiguracionVU listo para enviar + al API externo de COVE. + """ + vu = ctx.vu + company = self._get_company(ctx) + company_vu = company.ventanilla_unica if company else None + company_fiel_certificate = self._get_company_fiel_certificate(company) + + if not vu and not company_vu and not company_fiel_certificate: + errors.add_error( + field="vu", + message="La factura no tiene configuración VU asociada ni configuración VU/certificado FIEL en la empresa", + solution=[ + "Configura los datos VU del agente aduanal o de la empresa y sube certificado (.cer) y llave (.key) antes de generar COVE." + ], + code="MISSING_VU_CONFIGURATION", + ) + return None + + # Determinar usuario efectivo de WebService: + # - Preferimos el usuario configurado en VU (web_service_user) + # - Si no existe, usamos el de DODA-PITA (doda_web_service_user) + # - Si no existe, usamos la configuración VU de la empresa + effective_ws_user = ( + ( + vu.web_service_user + or vu.doda_web_service_user + or getattr(company_vu, "webservice_user", None) + or "" + ).strip() + if (vu or company_vu) + else "" + ) + + # Determinar clave FIEL efectiva desde la configuración persistida. + # Se envía cifrada con el mismo esquema AES-256-CBC del sistema legado. + clave_fiel_value = "" + + if vu and getattr(vu, "fiel_access_key", None): + clave_fiel_value = self._encrypt_fiel(vu.fiel_access_key or "") + elif company_fiel_certificate: + # Fallback en base de datos: certificado FIEL de la empresa + company_fiel_secret = ( + getattr(company_fiel_certificate, "access_key", None) + or getattr(company_fiel_certificate, "password", None) + or "" + ) + clave_fiel_value = self._encrypt_fiel(str(company_fiel_secret)) + + # Validación básica de credenciales VU: para COVE necesitamos al menos + # un usuario de web service (VU o DODA) y una clave FIEL no vacía. + if not effective_ws_user: + errors.add_error( + field="vu", + message="Faltan credenciales de web service o clave FIEL en VU", + solution=[ + "Captura usuario y clave de web service en la pestaña VU o DODA del agente, " + "o completa la configuración VU de la empresa y su certificado FIEL." + ], + code="MISSING_VU_CREDENTIALS", + ) + + if not clave_fiel_value: + errors.add_error( + field="vu.clave_fiel", + message="La clave FIEL para COVE no está configurada ni en VU ni en la empresa.", + solution=[ + "Captura la clave FIEL en la configuración VU del agente aduanal o en el certificado FIEL de la empresa." + ], + code="MISSING_FIEL_PASSWORD", + ) + + certificate_path = ( + (getattr(vu, "certificate_path", None) or "").strip() if vu else "" + ) or ( + (getattr(company_fiel_certificate, "cer_file_path", None) or "").strip() + if company_fiel_certificate + else "" + ) + key_path = ( + (getattr(vu, "key_path", None) or "").strip() if vu else "" + ) or ( + (getattr(company_fiel_certificate, "key_file_path", None) or "").strip() + if company_fiel_certificate + else "" + ) + + if not (certificate_path and key_path): + errors.add_error( + field="vu", + message="No hay rutas de certificado o llave en VU", + solution=[ + "Sube el certificado (.cer) y la llave (.key) en la configuración VU del agente aduanal o en los certificados digitales de la empresa." + ], + code="MISSING_VU_CERT_KEY", + ) + return None + + # Convertir archivos .cer y .key de S3 a base64 + cer_b64 = None + key_b64 = None + + try: + if not object_exists(certificate_path): + errors.add_error( + field="vu.certificate_path", + message="El certificado VU no existe en el almacenamiento de objetos", + solution=["Vuelve a subir el certificado en la configuración VU del agente aduanal o en los certificados digitales de la empresa."], + code="VU_CERT_NOT_FOUND", + ) + else: + cer_bytes = get_object_bytes(certificate_path) + cer_b64 = base64.b64encode(cer_bytes).decode("ascii") + + if not object_exists(key_path): + errors.add_error( + field="vu.key_path", + message="La llave VU no existe en el almacenamiento de objetos", + solution=["Vuelve a subir la llave en la configuración VU del agente aduanal o en los certificados digitales de la empresa."], + code="VU_KEY_NOT_FOUND", + ) + else: + key_bytes = get_object_bytes(key_path) + key_b64 = base64.b64encode(key_bytes).decode("ascii") + except Exception as exc: # pragma: no cover - errores de IO externos + errors.add_error( + field="vu", + message="Error leyendo certificados VU desde almacenamiento de objetos.", + solution=["Verifica la configuración de MinIO/S3 y las rutas de certificados/llaves en VU o en los certificados digitales de la empresa."], + code="VU_STORAGE_ERROR", + ) + + if errors.has_errors(): + return None + + rfc_usuario_vu = ( + (getattr(vu, "query_tax_id", None) or "").strip() if vu else "" + ) or ( + (getattr(company_vu, "query_rfc", None) or "").strip() if company_vu else "" + ) + + # Clave/token del webservice: usar el valor de VU si existe, o una + # clave fija de pruebas mientras se termina la configuración real. + hardcoded_ws_key = ( + "RZGd+CB4R6PfSrstOyN8Is9FXL9AK9NPFisyGGaEWa0vVHoVOl8v2SBcHBoGbt3T/4uHTGcsFQO3b7EonWVfugQjBooywbz74K+jM68j8/Y=" + ) + + return ConfiguracionVU( + rfc_usuario_vu=rfc_usuario_vu, + clave_webservice=( + (getattr(vu, "web_service_access_key", None) or "").strip() + or (getattr(company_vu, "webservice_password", None) or "").strip() + or hardcoded_ws_key + ), + archivo_cer_base64=cer_b64 or "", + archivo_key_base64=key_b64 or "", + clave_fiel=clave_fiel_value, + ) + + def _clientprovider_to_persona(self, cp: ClientProvider) -> PersonaCove: + """ + Construye una PersonaCove a partir de un ClientProvider + su dirección. + No expone IDs internos; solo valores normalizados. + """ + addr = cp.address + + tipo_nat = (cp.type_nat_foreign or "").strip().upper() + tipo_identificador = "0" if tipo_nat == "E" else "1" + identificacion = (cp.rfc or "").strip().upper() + + # País: normalizar a código de 3 caracteres (ISO o catálogo VU). + raw_country = (addr.country or "") if addr and getattr(addr, "country", None) else "" + country_code = raw_country.strip().upper()[:3] if raw_country else "" + + return PersonaCove( + tipo_identificador=tipo_identificador, + identificacion=identificacion, + apellido_paterno="", + apellido_materno="", + nombre=(cp.name or cp.short_name or "").strip() or None, + calle=(addr.streets or "").strip() if addr and addr.streets else None, + numero_exterior=(addr.exterior_number or "").strip() + if addr and addr.exterior_number + else None, + # El API de COVE exige texto (no null) para numero_interior y municipio. + # Si no hay valor, enviamos cadena vacía. + numero_interior=(addr.interior_number or "").strip() + if addr + else "", + colonia=(addr.neighborhood or "").strip() + if addr and addr.neighborhood + else None, + localidad=(addr.city or "").strip() if addr and addr.city else None, + municipio=(addr.municipality or "").strip() + if addr + else "", + entidad_federativa=(addr.state or "").strip() + if addr and addr.state + else None, + pais=country_code, + codigo_postal=(addr.postal_code or "").strip() + if addr and addr.postal_code + else None, + ) + + def _company_to_persona(self, company: Company) -> PersonaCove: + """ + Construye una PersonaCove a partir de Company + su dirección principal. + """ + # Tomar dirección 'main' si existe; si no, la primera. + addr = None + for a in company.addresses or []: + if getattr(a, "address_type", None) == "main": + addr = a + break + if addr is None and company.addresses: + addr = company.addresses[0] + + # País: normalizar a código de 3 caracteres (ISO o catálogo VU). + raw_country = (addr.country or "") if addr and getattr(addr, "country", None) else "" + country_code = raw_country.strip().upper()[:3] if raw_country else "" + + return PersonaCove( + tipo_identificador="1", # Empresa mexicana por defecto + identificacion=(company.rfc or "").strip().upper(), + apellido_paterno="", + apellido_materno="", + nombre=(company.name or "").strip() or None, + calle=(addr.street or "").strip() if addr and addr.street else None, + numero_exterior=(addr.exterior_number or "").strip() + if addr and addr.exterior_number + else None, + numero_interior=(addr.interior_number or "").strip() + if addr + else "", + colonia=(addr.neighborhood or "").strip() + if addr and addr.neighborhood + else None, + localidad=(addr.city or "").strip() if addr and addr.city else None, + municipio=(addr.municipality or "").strip() + if addr + else "", + entidad_federativa=(addr.state or "").strip() + if addr and addr.state + else None, + pais=country_code, + codigo_postal=(addr.postal_code or "").strip() + if addr and addr.postal_code + else None, + ) + + def _build_personas( + self, ctx: InvoiceContext, errors: ErrorCollector + ) -> Tuple[PersonaCove | None, PersonaCove | None]: + """ + Construye emisor (exportador) y destinatario (importador) a partir de: + - InvoiceComplianceMx.provider_id / sold_to_id / shipped_to_id + - Catálogo de clientes/proveedores + - Company (datos de la propia empresa) como último recurso + """ + compliance = ctx.invoice.compliance_mx + tenant_id = getattr(ctx.invoice, "tenant_id", None) + company_id = getattr(ctx.invoice, "company_id", None) + + emisor_persona: PersonaCove | None = None + destinatario_persona: PersonaCove | None = None + + # --- Emisor: proveedor/exportador --- + if compliance and compliance.provider_id: + provider = ( + self.db.query(ClientProvider) + .filter( + ClientProvider.id == compliance.provider_id, + ClientProvider.tenant_id == tenant_id, + ClientProvider.company_id == company_id, + ) + .first() + ) + if provider: + emisor_persona = self._clientprovider_to_persona(provider) + else: + errors.add_error( + field="emisor", + message="No se encontró el proveedor/exportador asociado a la factura", + solution=[ + "Verifica que el proveedor/exportador exista en el catálogo y que el invoice_compliance_mx.provider_id sea válido." + ], + code="EMISOR_PROVIDER_NOT_FOUND", + ) + else: + errors.add_error( + field="emisor", + message="La factura no tiene proveedor/exportador configurado en cumplimiento (provider_id)", + solution=[ + "Configura el proveedor/exportador (provider_id) en los datos de cumplimiento de la factura." + ], + code="EMISOR_PROVIDER_MISSING", + ) + + # --- Destinatario: importador mexicano --- + dest_client: ClientProvider | None = None + if compliance and compliance.sold_to_id: + dest_client = ( + self.db.query(ClientProvider) + .filter( + ClientProvider.id == compliance.sold_to_id, + ClientProvider.tenant_id == tenant_id, + ClientProvider.company_id == company_id, + ) + .first() + ) + elif compliance and compliance.shipped_to_id: + dest_client = ( + self.db.query(ClientProvider) + .filter( + ClientProvider.id == compliance.shipped_to_id, + ClientProvider.tenant_id == tenant_id, + ClientProvider.company_id == company_id, + ) + .first() + ) + + if dest_client: + destinatario_persona = self._clientprovider_to_persona(dest_client) + else: + # Fallback: usar la empresa de A76 como importador/destinatario + if company_id is not None: + company = ( + self.db.query(Company) + .filter(Company.id == company_id, Company.tenant_id == tenant_id) + .first() + ) + else: + company = None + + if company: + destinatario_persona = self._company_to_persona(company) + else: + errors.add_error( + field="destinatario", + message="No se pudo determinar el destinatario (cliente/importador) para COVE", + solution=[ + "Configura sold_to_id o shipped_to_id en los datos de cumplimiento de la factura, " + "o asegura que la compañía tenga datos de dirección configurados." + ], + code="DESTINATARIO_NOT_FOUND", + ) + + return emisor_persona, destinatario_persona + + def _build_mercancias(self, ctx: InvoiceContext, errors: ErrorCollector) -> list[MercanciaCove]: + """ + Construye la lista de mercancías COVE a partir de las partidas (LineItem) + asociadas a la factura. + """ + # Obtener todas las partidas de la factura + lines: list[LineItem] = ( + self.db.query(LineItem) + .filter(LineItem.invoice_id == ctx.invoice.id) + .all() + ) + + if not lines: + errors.add_error( + field="mercancias", + message="La factura no tiene partidas (LineItem) asociadas", + solution=[ + "Verifica que la factura tenga partidas capturadas antes de generar COVE." + ], + code="NO_LINE_ITEMS_FOR_COVE", + ) + return [] + + mercancias: list[MercanciaCove] = [] + + # Determinar moneda base: usamos la moneda de la factura tal como + # la maneja el módulo de invoices. En financials se normaliza: + # - currency: 'foreign' | 'local' | 'manual' + # - currency_type: código de catálogo (ej. 'USD', 'MXN'), upper. + fin = getattr(ctx.invoice, "financials", None) + raw_currency_type = getattr(fin, "currency_type", None) + invoice_currency = (raw_currency_type or "").strip().upper() or "USD" + + for line in lines: + qty_model = line.quantity + fin_model = line.financial + desc_model = line.description + + if not qty_model or qty_model.quantity is None or qty_model.quantity <= 0: + # Saltar partidas sin cantidad válida + continue + + # Cantidad: normalizar a EXACTAMENTE 2 decimales (ej. 12.23) + cantidad = Decimal(str(qty_model.quantity)).quantize(Decimal("0.01")) + + # Descripción genérica: priorizar descripción de parte / inglés / español + descripcion = "" + if desc_model: + descripcion = ( + desc_model.part_description + or desc_model.description_english + or desc_model.description_spanish + or "" + ).strip() + if not descripcion: + descripcion = (line.line_concept or "").strip() + if not descripcion: + descripcion = "SIN DESCRIPCION" + + # Clave unidad de medida: usar OMA/customs si están disponibles + clave_unidad = "" + uom = line.unit_of_measure_info + if uom: + if uom.oma_unit and uom.oma_unit.code: + clave_unidad = uom.oma_unit.code + elif uom.customs_unit and uom.customs_unit.code: + clave_unidad = uom.customs_unit.code + elif uom.code: + clave_unidad = uom.code + + if not clave_unidad: + errors.add_error( + field="mercancias", + message="No se pudo determinar la unidad de medida para una partida de la factura", + solution=[ + "Asegúrate de que la partida tenga una unidad de medida configurada en el catálogo " + "y que esté ligada a una clave OMA/aduana válida." + ], + code="MERCANCIA_UOM_MISSING", + ) + continue + + # Moneda y valores: usamos la moneda de la factura (3 caracteres) + tipo_moneda = invoice_currency + + valor_total = Decimal("0") + valor_dolares = Decimal("0") + valor_unitario = Decimal("0") + + if fin_model: + if tipo_moneda == "USD": + base_total = ( + fin_model.value_total_usd + or fin_model.value_usd + or Decimal("0") + ) + else: + # Para otras monedas, usamos el total en MXN/MC como respaldo + base_total = ( + fin_model.value_total_mxn + or fin_model.value_mxn + or fin_model.value_total_mc + or fin_model.value_mc + or Decimal("0") + ) + + # Valor total: normalizar a EXACTAMENTE 2 decimales + valor_total = Decimal(str(base_total or 0)).quantize(Decimal("0.01")) + if cantidad > 0: + # Valor unitario también a EXACTAMENTE 2 decimales + valor_unitario = (valor_total / cantidad).quantize(Decimal("0.01")) + else: + valor_unitario = Decimal("0") + + # Valor en dólares: si ya existe, lo usamos; si no, asumimos que los totales ya están en USD. + if fin_model.value_total_usd: + valor_dolares = Decimal(str(fin_model.value_total_usd)) + elif tipo_moneda == "USD": + valor_dolares = valor_total + else: + valor_dolares = Decimal("0") + + # Normalizar valor en dólares a EXACTAMENTE 2 decimales + valor_dolares = valor_dolares.quantize(Decimal("0.01")) + else: + errors.add_error( + field="mercancias", + message="La partida de la factura no tiene información financiera asociada", + solution=[ + "Verifica que las partidas tengan datos financieros (LineFinancial) antes de generar COVE." + ], + code="MERCANCIA_FINANCIAL_MISSING", + ) + continue + + mercancia = MercanciaCove( + descripcion_generica=descripcion[:500], + clave_unidad_medida=clave_unidad, + tipo_moneda=tipo_moneda, + cantidad=cantidad, + valor_unitario=valor_unitario, + valor_total=valor_total, + valor_dolares=valor_dolares, + descripcion_especifica=[], + ) + mercancias.append(mercancia) + + if not mercancias and not errors.has_errors(): + errors.add_error( + field="mercancias", + message="No se generó ninguna mercancía COVE a partir de las partidas de la factura", + solution=[ + "Verifica que las partidas tengan cantidad y datos financieros válidos antes de generar COVE." + ], + code="NO_MERCANCIAS_GENERATED", + ) + + return mercancias + + def build_factura_cove_request( + self, + invoice_id: int, + tenant_id: int, + company_id: int, + recipient_email: str | None = None, + ) -> FacturaCoveRequest: + """ + Construye el FacturaCoveRequest completo a partir de una factura, + validando prerrequisitos de VU, factura y mapeos. + """ + ctx = self._load_context(invoice_id, tenant_id, company_id) + errors = ErrorCollector() + + # Validaciones básicas de factura + if not ctx.invoice.invoice_number: + errors.add_error( + field="invoice.invoice_number", + message="La factura no tiene número de factura", + solution=["Captura el número de factura antes de generar COVE."], + code="MISSING_INVOICE_NUMBER", + ) + + # Construir configuración VU (puede agregar errores) + configuracion_vu = self._build_configuracion_vu(ctx, errors) + + # Personas y mercancías (por ahora placeholders con errores explícitos) + emisor, destinatario = self._build_personas(ctx, errors) + mercancias = self._build_mercancias(ctx, errors) + + if errors.has_errors(): + # Levantamos ValidationException con todos los errores + raise ValidationException("No se puede generar COVE desde la factura", errors=errors.get_errors()) + + # Campos genéricos que se pueden poblar de forma segura + raw_tipo_operacion = (ctx.invoice.operation_type or "").strip().lower() + # Mapear tipo_operacion al código esperado por el API de COVE + # Ejemplos: + # - IMP / importación -> "TOCE.IMP" + # - EXP / exportación -> "TOCE.EXP" + if raw_tipo_operacion in {"imp", "import", "importacion", "importación"}: + tipo_operacion = "TOCE.IMP" + elif raw_tipo_operacion in {"exp", "export", "exportacion", "exportación"}: + tipo_operacion = "TOCE.EXP" + else: + # Fallback seguro: usar valor por defecto de importación + tipo_operacion = "TOCE.IMP" + numero_factura = (ctx.invoice.invoice_number or "").strip()[:50] + fecha_expedicion = ctx.invoice.invoice_date or ctx.invoice.emission_date or ctx.invoice.capture_date + + if not fecha_expedicion: + raise ValidationException( + "Falta fecha de expedición de factura", + errors=[ + { + "field": "invoice.invoice_date", + "message": "La factura no tiene fecha de expedición/emisión/captura", + "solution": ["Captura la fecha de la factura antes de generar COVE."], + } + ], + ) + + # Patente aduanal en mayúsculas y acotada a 10 caracteres + raw_patente = (ctx.broker.license if ctx.broker else "") or "" + patente_aduanal = raw_patente.strip().upper()[:10] + + # Normalizar tipo_figura desde VU: el API externo espera un código corto + # (en el ejemplo: "5" para agente aduanal). Hacemos un mapeo simple + # desde el texto configurado en la UI. + raw_figura = ( + (ctx.vu.vu_figure_type or "").strip().upper() + if ctx.vu and ctx.vu.vu_figure_type + else "" + ) + if "AGENTE" in raw_figura: + tipo_figura = "5" + elif "APODERADO" in raw_figura: + tipo_figura = "6" + elif "MANDATARIO" in raw_figura: + tipo_figura = "7" + else: + # Fallback: recortar a máximo 10 caracteres para cumplir el esquema + tipo_figura = raw_figura[:10] + + correo_destino = (recipient_email or (ctx.vu.vu_email if ctx.vu else None) or "").strip() or None + + return FacturaCoveRequest( + configuracion_vu=configuracion_vu, + # El RFC de consulta NO debe ser igual al RFC del que registra el comprobante. + # Usamos como RFC de consulta el RFC del agente aduanal (customs broker), + # y dejamos que configuracion_vu.rfc_usuario_vu represente al contribuyente. + rfc_consulta=( + (ctx.broker.tax_id or "").strip().upper() if ctx.broker and ctx.broker.tax_id else "" + ), + tipo_figura=tipo_figura, + numero_factura=numero_factura, + tipo_operacion=tipo_operacion, + patente_aduanal=patente_aduanal, + fecha_expedicion=fecha_expedicion, + observaciones=ctx.invoice.vu_observations or None, + correo_electronico=correo_destino, + tiene_subdivision=bool(ctx.invoice.logistics and ctx.invoice.logistics.is_subdivision), + certificado_origen=False, + numero_exportador_autorizado=None, + emisor=emisor, # type: ignore[arg-type] + destinatario=destinatario, # type: ignore[arg-type] + mercancias=mercancias, + ) + + def check_eligibility(self, invoice_id: int, tenant_id: int, company_id: int) -> CoveEligibilityResponse: + """ + Versión "ligera" para frontend: evalúa si la factura puede generar COVE + e informa por qué no, sin disparar la tarea Celery. + """ + errors = ErrorCollector() + + try: + ctx = self._load_context(invoice_id, tenant_id, company_id) + # Reutilizamos solo las validaciones, sin necesidad de devolver el request completo + if not ctx.invoice.invoice_number: + errors.add_error( + field="invoice.invoice_number", + message="La factura no tiene número de factura", + solution=["Captura el número de factura antes de generar COVE."], + code="MISSING_INVOICE_NUMBER", + ) + + self._build_configuracion_vu(ctx, errors) + self._build_personas(ctx, errors) + self._build_mercancias(ctx, errors) + except ValidationException as exc: + # Errores de load_context (factura no existe, compañía distinta, etc.) + return CoveEligibilityResponse( + can_generate=False, + reasons=[CoveEligibilityIssue(field=e.get("field", ""), message=e.get("message", "")) for e in exc.errors], + ) + + if not errors.has_errors(): + return CoveEligibilityResponse(can_generate=True, reasons=[]) + + return CoveEligibilityResponse( + can_generate=False, + reasons=[ + CoveEligibilityIssue(field=e.get("field", ""), message=e.get("message", "")) + for e in errors.get_errors() + ], + ) + diff --git a/backend/api/v1/modules/a76/factura_cove/tasks.py b/backend/api/v1/modules/a76/factura_cove/tasks.py new file mode 100644 index 00000000..76a6a09c --- /dev/null +++ b/backend/api/v1/modules/a76/factura_cove/tasks.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +import logging +import time +from typing import Any, Dict + +from celery import Task + +from core.celery_app import celery_app +from core.database import CoreSessionLocal +from core.exceptions import ValidationException +from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx + +from .service import FacturaCoveDomainService +from .schemas import GenerateCoveResult +from .external_service import CoveExternalService, CoveExternalResult + +logger = logging.getLogger(__name__) + + +def _progress(task: Task, current: int, status: str) -> None: + task.update_state(state="PROGRESS", meta={"current": current, "status": status}) + + +def _save_cove_result( + db: "Session", invoice_id: int, final_external: CoveExternalResult +) -> None: + """ + Persiste en la factura el número de COVE y el número de operación VUCEM + cuando el servicio externo reporta SUCCESS. + """ + if final_external.status != "success": + return + + if not final_external.cove_number and not final_external.vucem_operation_num: + return + + invoice = db.get(InvoiceHeader, invoice_id) + if not invoice: + logger.error("No se encontró la factura %s para guardar COVE", invoice_id) + return + + compliance = invoice.compliance_mx + if not compliance: + # Creamos un registro mínimo de compliance ligado a la factura. + compliance = InvoiceComplianceMx( + invoice_id=invoice.id, + tenant_id=invoice.tenant_id, + company_id=invoice.company_id, + ) + db.add(compliance) + + # Idempotencia básica: solo sobrescribir si está vacío o coincide. + if final_external.cove_number: + current_cove = compliance.edocument or "" + new_cove = final_external.cove_number or "" + if not current_cove or current_cove == new_cove: + compliance.edocument = new_cove + + if final_external.vucem_operation_num: + current_op = compliance.vucem_operation_num or "" + new_op = final_external.vucem_operation_num or "" + if not current_op or current_op == new_op: + compliance.vucem_operation_num = new_op + + db.commit() + + +def _poll_external_status( + task: Task, external: CoveExternalService, external_task_id: str, timeout_seconds: int = 300 +) -> CoveExternalResult: + """ + Realiza polling al endpoint externo de status de COVE hasta obtener un estado final + o agotar el timeout. + """ + start = time.time() + last_payload: Dict[str, Any] = {} + + while True: + if time.time() - start > timeout_seconds: + logger.error("Timeout consultando estado de COVE para external_task_id=%s", external_task_id) + return CoveExternalResult( + status="error", + message="Timeout consultando estado de COVE en Ventanilla Única.", + cove_number=None, + vucem_operation_num=None, + raw_response={"last_status": last_payload, "external_task_id": external_task_id}, + ) + + try: + status_payload = external.get_status(external_task_id) + except Exception as exc: # pragma: no cover - errores HTTP inesperados + logger.exception("Error consultando estado externo de COVE") + return CoveExternalResult( + status="error", + message=f"Error consultando estado de COVE en Ventanilla Única: {exc}", + cove_number=None, + vucem_operation_num=None, + raw_response={"last_status": last_payload, "external_task_id": external_task_id}, + ) + + last_payload = status_payload or {} + state = str(last_payload.get("state") or "").upper() + progress = last_payload.get("progress") or {} + # En el ejemplo: progress.progress (float 0-100), progress.current_step (texto), numero_operacion + try: + percent = float(progress.get("progress", 0.0)) + except (TypeError, ValueError): + percent = 0.0 + current_step = progress.get("current_step") or "Consultando estado de COVE en Ventanilla Única..." + numero_operacion = progress.get("numero_operacion") or last_payload.get("numero_operacion") + if numero_operacion: + current_step = f"{current_step} (Operación: {numero_operacion})" + + # Actualizar progreso para que el frontend lo vea en el diálogo + _progress(task, int(percent), str(current_step)) + + # Estados intermedios: seguimos pollendo + if state in {"PENDING", "STARTED", "PROGRESS"} or not state: + time.sleep(5) + continue + + # Estado final: SUCCESS / FAILURE u otros + result_payload = last_payload.get("result") or {} + error_text = last_payload.get("error") + + if state in {"SUCCESS", "COMPLETED"}: + cove_number = result_payload.get("cove_number") or result_payload.get("cove") + vucem_operation_num = result_payload.get("vucem_operation_num") or result_payload.get( + "numero_operacion" + ) + message = result_payload.get("message") or last_payload.get("message") or "COVE generado correctamente." + + return CoveExternalResult( + status="success", + message=message, + cove_number=cove_number, + vucem_operation_num=vucem_operation_num, + raw_response={**last_payload, "external_task_id": external_task_id}, + ) + + # Cualquier otro estado lo tratamos como error + message = error_text or result_payload.get("message") or last_payload.get("message") or state + + return CoveExternalResult( + status="error", + message=str(message), + cove_number=None, + vucem_operation_num=None, + raw_response={**last_payload, "external_task_id": external_task_id}, + ) + + +@celery_app.task(bind=True, name="factura_cove_generate") +def factura_cove_generate( + self: Task, + invoice_id: int, + tenant_id: int, + company_id: int, + recipient_email: str | None = None, +) -> dict: + """ + Tarea Celery para preparar (y en el futuro generar) un COVE a partir de una factura. + + Actualmente: + - Valida prerrequisitos de factura y configuración VU. + - Construye el payload FacturaCoveRequest (sin llamar aún al webservice externo). + - Devuelve un resultado estándar indicando éxito o errores de validación. + + En el futuro se puede extender para: + - Invocar al servicio externo de COVE. + - Persistir número de COVE / operación VUCEM en la factura. + """ + db = CoreSessionLocal() + + try: + _progress(self, 5, "Validando factura para COVE...") + service = FacturaCoveDomainService(db) + + # Esta llamada valida todo y construye el payload; si algo falla, lanza ValidationException + request_payload = service.build_factura_cove_request( + invoice_id=invoice_id, + tenant_id=tenant_id, + company_id=company_id, + recipient_email=recipient_email, + ) + + _progress(self, 80, "Enviando solicitud al servicio COVE...") + + # Integración externa que encola la generación de COVE en Ventanilla Única + external = CoveExternalService() + external_result = external.generate_cove(request_payload) + + # Si el servicio externo devolvió un error inmediato (por ejemplo 422), + # devolvemos ese resultado tal cual sin hacer polling adicional. + if external_result.status in {"error", "validation_error"}: + result = GenerateCoveResult( + status=external_result.status, + message=external_result.message, + invoice_id=invoice_id, + cove_number=external_result.cove_number, + vucem_operation_num=external_result.vucem_operation_num, + external_task_id=( + external_result.raw_response.get("task_id") if external_result.raw_response else None + ), + external_response=external_result.raw_response, + errors=None, + ) + return result.model_dump() + + external_task_id = ( + external_result.raw_response.get("task_id") if external_result.raw_response else None + ) + + # Si la factura quedó encolada en VU y tenemos un task_id externo, hacemos polling + # al endpoint de status para acompañar el progreso completo hasta obtener COVE. + if external_task_id and external_result.status in {"external_queued", "queued", "success"}: + _progress( + self, + 85, + "Factura enviada a Ventanilla Única, consultando estado de COVE...", + ) + final_external = _poll_external_status(self, external, external_task_id) + else: + # Fallback: usamos el resultado tal cual devolvió el endpoint de generación + final_external = external_result + + # Intentar persistir COVE / número de operación en la factura cuando sea éxito. + try: + _save_cove_result(db, invoice_id, final_external) + except Exception: + # No fallamos la tarea por errores de persistencia; solo los registramos. + logger.exception("Error guardando COVE en la factura %s", invoice_id) + + _progress(self, 100, "Proceso de COVE finalizado.") + + result = GenerateCoveResult( + status=final_external.status, + message=final_external.message, + invoice_id=invoice_id, + cove_number=final_external.cove_number, + vucem_operation_num=final_external.vucem_operation_num, + external_task_id=( + final_external.raw_response.get("external_task_id") + or final_external.raw_response.get("task_id") + if final_external.raw_response + else None + ), + external_response=final_external.raw_response, + errors=None, + ) + return result.model_dump() + + except ValidationException as exc: + db.rollback() + logger.info("Validation error in factura_cove_generate: %s", exc.message) + result = GenerateCoveResult( + status="validation_error", + message=exc.message, + invoice_id=invoice_id, + errors=exc.errors, + ) + return result.model_dump() + except Exception as exc: # pragma: no cover - errores inesperados de runtime + db.rollback() + logger.exception("Unexpected error in factura_cove_generate") + result = GenerateCoveResult( + status="error", + message=str(exc), + invoice_id=invoice_id, + errors=None, + ) + return result.model_dump() + finally: + db.close() + diff --git a/backend/api/v1/modules/a76/general_catalogs/company/routes.py b/backend/api/v1/modules/a76/general_catalogs/company/routes.py index d6871d64..31eb0912 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/routes.py @@ -432,11 +432,18 @@ async def upload_company_certificate( detail=f"File type not allowed. Allowed: {', '.join(allowed_exts)}", ) - # Validar correspondencia extensión vs tipo (simple check) - if "cer" in certificate_type and file_ext != ".cer": - raise HTTPException(status_code=400, detail="For this certificate type, file must be .cer") - if "key" in certificate_type and file_ext != ".key": - raise HTTPException(status_code=400, detail="For this certificate type, file must be .key") + # Validar correspondencia extensión vs tipo (simple check, respetando sufijo) + ctype = (certificate_type or "").lower() + if ctype.endswith("_cer") and file_ext != ".cer": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="For this certificate type, file must be .cer", + ) + if ctype.endswith("_key") and file_ext != ".key": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="For this certificate type, file must be .key", + ) # Validar tamaño content = await file.read() diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py index 09a607df..a58756f6 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py @@ -3,9 +3,10 @@ Service layer for Pedimentos CRUD operations """ import logging +import re from typing import Any, Dict, List, Optional -from sqlalchemy import desc +from sqlalchemy import desc, func from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import selectinload from sqlalchemy.exc import IntegrityError @@ -95,10 +96,21 @@ class PedimentosService: if filters.get("status"): query = query.filter(Pedimentos.status == filters["status"]) if filters.get("client_id"): - query = query.filter( - Pedimentos.client_id == filters["client_id"]) + query = query.filter(Pedimentos.client_id == filters["client_id"]) if filters.get("year"): query = query.filter(Pedimentos.year == filters["year"]) + if filters.get("pedimento"): + raw_value = str(filters["pedimento"]) + # Normalizar: dejar solo dígitos (ignorar guiones, espacios, etc.) + normalized = re.sub(r"\D", "", raw_value) + if normalized: + ped_key_expr = func.concat( + func.coalesce(Pedimentos.year, ""), + func.coalesce(func.substr(Pedimentos.customs_office, 1, 2), ""), + func.coalesce(Pedimentos.license, ""), + func.coalesce(Pedimentos.pedimento_number, ""), + ) + query = query.filter(ped_key_expr.ilike(f"%{normalized}%")) total = query.count() diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 7d8df946..ed0a6600 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -34,6 +34,7 @@ from .transportation.trailers.routes import router as trailers_router from .transportation.transporters.routes import router as transporters_router from .transportation.vehicles.routes import router as vehicles_router from api.v1.modules.public.reference_data.material_types.routes import router as material_types_router +from .factura_cove.routes import router as factura_cove_router # --- NUEVO IMPORT PARA REPORTES DE FACTURAS --- from .reports.importacion.facturas.routes import router as invoices_reports_router @@ -83,6 +84,7 @@ router.include_router(doc_types_dig_router, prefix="/a76", tags=["a76 / document router.include_router(drivers_router, prefix="/a76", tags=["a76 / drivers"]) router.include_router(transporters_router, prefix="/a76", tags=["a76 / transporters"]) router.include_router(vehicles_router, prefix="/a76/transportation", tags=["a76 / vehicles"]) +router.include_router(factura_cove_router, prefix="/a76/factura-cove", tags=["a76 / factura_cove"]) # Registrar router de tipos de material públicos router.include_router( diff --git a/backend/api/v1/modules/public/reference_data/incoterms/routes.py b/backend/api/v1/modules/public/reference_data/incoterms/routes.py index 02b278fc..261cfddc 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/routes.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/routes.py @@ -15,11 +15,22 @@ router = APIRouter(prefix="/incoterms") async def list_incoterms( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + code: str = Query(None, description="Filtrar por clave"), + description: str = Query(None, description="Filtrar por descripción"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(Incoterm) + + if code: + query = query.filter(Incoterm.code.ilike(f"%{code}%")) + if description: + query = query.filter( + (Incoterm.description_es.ilike(f"%{description}%")) | + (Incoterm.description_en.ilike(f"%{description}%")) + ) + items = query.offset(skip).limit(page_size).all() total = query.count() return { diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index d1dae953..617a45f4 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -72,6 +72,7 @@ celery_app.conf.update( "api.v1.modules.a76.invoices.exports.process.task", "api.v1.modules.a76.invoices.exports.revert.task", "api.v1.modules.a76.layouts_csv.common.victor", + "api.v1.modules.a76.factura_cove.tasks", ] # Ruta al módulo donde están las tareas ) diff --git a/backend/core/config.py b/backend/core/config.py index 7ad763b5..bcda63cc 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -3,7 +3,6 @@ Configuración centralizada de la aplicación usando Pydantic Settings """ from typing import List, Literal - from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -53,6 +52,9 @@ class Settings(BaseSettings): # External APIs SITAR_API_URL: str = "api.sitar.aduanasoft.com:880" + COVE_API_URL: str = "" + COVE_FIEL_HASH_KEY: str = "" + COVE_FIEL_HASH_IV: str = "" SITAR_API_USER: str = "" SITAR_API_PASSWORD: str = "" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 75af28a0..cceace5d 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -173,7 +173,9 @@ services: - CORS_ORIGINS=${CORS_ORIGINS:-https://anexo76-dev.aduanasoft.com,http://localhost:3000} - SITAR_API_URL=${SITAR_API_URL} - SITAR_API_USER=${SITAR_API_USER} - - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} + - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} - VALKEY_URL=${VALKEY_URL:-redis://valkey:6379/0} - CENTRAL_SERVER_URL=${CENTRAL_SERVER_URL:-""} - SYNC_SECRET_TOKEN=${SYNC_SECRET_TOKEN:-change-this-sync-token-in-production} @@ -242,6 +244,8 @@ services: - SITAR_API_URL=${SITAR_API_URL} - SITAR_API_USER=${SITAR_API_USER} - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} + - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} @@ -275,6 +279,8 @@ services: - SITAR_API_URL=${SITAR_API_URL} - SITAR_API_USER=${SITAR_API_USER} - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} + - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} diff --git a/docker-compose.yml b/docker-compose.yml index 68d14cd0..80322b76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,6 +178,8 @@ services: - SITAR_API_URL=${SITAR_API_URL} - SITAR_API_USER=${SITAR_API_USER} - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} + - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} - VALKEY_URL=${VALKEY_URL:-redis://valkey:6379/0} - CENTRAL_SERVER_URL=${CENTRAL_SERVER_URL:-""} - SYNC_SECRET_TOKEN=${SYNC_SECRET_TOKEN:-change-this-sync-token-in-production} @@ -304,6 +306,8 @@ services: - SITAR_API_URL=${SITAR_API_URL} - SITAR_API_USER=${SITAR_API_USER} - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} + - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} @@ -336,6 +340,11 @@ services: - CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core} - CORE_DB_USER=${CORE_DB_USER:-postgres} - CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres} + - SITAR_API_URL=${SITAR_API_URL} + - SITAR_API_USER=${SITAR_API_USER} + - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} + - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} + - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} diff --git a/frontend/src/app.css b/frontend/src/app.css index 6c344082..627e3836 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -121,3 +121,45 @@ @apply bg-background text-foreground overflow-x-hidden; } } + +@layer components { + .catalog-table-shell { + @apply rounded-md border border-border/80 bg-card shadow-sm; + } + + .catalog-table-scroll { + @apply relative w-full flex-1 overflow-auto bg-card; + } + + .catalog-table-header { + @apply sticky top-0 z-20 border-b border-border/80 bg-card/95 shadow-sm backdrop-blur-md; + } + + .catalog-table-head-cell { + @apply whitespace-nowrap text-sm font-semibold text-foreground/90; + } + + .catalog-table-row { + @apply transition-colors hover:bg-accent/35; + } + + .catalog-table-row-selected { + @apply bg-accent/65 text-accent-foreground hover:bg-accent/65; + } + + .catalog-table-sticky-left { + @apply sticky left-0 border-r border-border/70 bg-card shadow-[4px_0_12px_-6px_rgba(0,0,0,0.12)] dark:shadow-[4px_0_12px_-6px_rgba(0,0,0,0.35)]; + } + + .catalog-table-sticky-right { + @apply sticky right-0 border-l border-border/70 bg-card shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.08)] dark:shadow-[-4px_0_12px_-6px_rgba(0,0,0,0.25)]; + } + + .catalog-table-sticky-row-hover { + @apply bg-card group-hover/inv-list:bg-accent/35; + } + + .catalog-table-sticky-row-selected { + @apply bg-accent/65 text-accent-foreground; + } +} diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts index fb415397..a565026a 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts @@ -40,11 +40,14 @@ import type { ApiResponse } from '$lib/api'; export async function getMultiCurrencyTypes( companyId: number, page?: number, - pageSize?: number + pageSize?: number, + filters?: { currency_type_code?: string; country_key?: string } ): Promise> { const params = new URLSearchParams({ company_id: companyId.toString() }); if (page) params.append('page', page.toString()); if (pageSize) params.append('page_size', pageSize.toString()); + if (filters?.currency_type_code) params.append('currency_type_code', filters.currency_type_code); + if (filters?.country_key) params.append('country_key', filters.country_key); return api.get(`/v1/a76/multi-currency-types/?${params.toString()}`); } diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index fb3c491c..e8828c7d 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -543,5 +543,46 @@ export const invoicesApi = { sql_errors?: Array<{ consecutive: number; error: string }>; }; }>(`/v1/a76/invoices/revert/${taskId}/status`); + }, + + generateCove: (invoiceId: number, companyId: number, recipientEmail?: string | null) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + const body: Record = { + company_id: companyId + }; + if (recipientEmail) { + body.recipient_email = recipientEmail; + } + return api.post<{ task_id: string }>( + `/v1/a76/factura-cove/invoices/${invoiceId}/cove?${params.toString()}`, + body + ); + }, + + getCoveStatus: (taskId: string) => { + return api.get<{ + state: 'PROCESSING' | 'SUCCESS' | 'FAILURE'; + info?: { current: number; status: string }; + result?: { + status: 'success' | 'validation_error' | 'error'; + invoice_id?: number; + cove_number?: string; + vucem_operation_num?: string; + message?: string; + errors?: Array<{ field: string; message: string; code?: string; solution?: string[] }>; + }; + }>(`/v1/a76/factura-cove/invoices/cove/${taskId}/status`); + }, + + checkCoveEligibility: (invoiceId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get<{ + can_generate: boolean; + reasons: Array<{ field: string; message: string }>; + }>(`/v1/a76/factura-cove/invoices/${invoiceId}/cove/eligibility?${params.toString()}`); } }; diff --git a/frontend/src/lib/api/dashboard/a76/pedimentos.ts b/frontend/src/lib/api/dashboard/a76/pedimentos.ts index dd876a08..9c5e93ec 100644 --- a/frontend/src/lib/api/dashboard/a76/pedimentos.ts +++ b/frontend/src/lib/api/dashboard/a76/pedimentos.ts @@ -273,6 +273,7 @@ export interface UpdatePedimentoData { export interface PedimentoFilters { status?: string; client_id?: number; + pedimento?: string; year?: string; sort_by?: string; sort_order?: 'asc' | 'desc' | string; diff --git a/frontend/src/lib/api/dashboard/reference_data/code_pedimento_regimens.ts b/frontend/src/lib/api/dashboard/reference_data/code_pedimento_regimens.ts index 4b927437..7bb4fa45 100644 --- a/frontend/src/lib/api/dashboard/reference_data/code_pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/reference_data/code_pedimento_regimens.ts @@ -35,13 +35,18 @@ export interface UpdateCodePedimentoRegimenData { */ export const codePedimentoRegimensApi = { /** - * Lista todos los code pedimento regimens con paginación + * Lista todos los code pedimento regimens con paginación y búsqueda + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORRECTO: Slash antes del signo '?' - `/v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un por ID diff --git a/frontend/src/lib/api/dashboard/reference_data/containers.ts b/frontend/src/lib/api/dashboard/reference_data/containers.ts index 67f240d6..9e4d300b 100644 --- a/frontend/src/lib/api/dashboard/reference_data/containers.ts +++ b/frontend/src/lib/api/dashboard/reference_data/containers.ts @@ -31,15 +31,18 @@ export interface UpdateContainerData { */ export const containersApi = { /** - * Lista todos los containers con paginación + * Lista todos los containers con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORRECTO: Slash antes del ? - `/v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un container por ID diff --git a/frontend/src/lib/api/dashboard/reference_data/currency_types.ts b/frontend/src/lib/api/dashboard/reference_data/currency_types.ts index 387e1f3f..9199365f 100644 --- a/frontend/src/lib/api/dashboard/reference_data/currency_types.ts +++ b/frontend/src/lib/api/dashboard/reference_data/currency_types.ts @@ -34,15 +34,18 @@ export interface UpdateCurrencyTypeData { */ export const currencyTypesApi = { /** - * Lista todos los tipos de moneda con paginación + * Lista todos los tipos de moneda con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un tipo de moneda por código diff --git a/frontend/src/lib/api/dashboard/reference_data/customs_sections.ts b/frontend/src/lib/api/dashboard/reference_data/customs_sections.ts index e73934d8..e0cafb6e 100644 --- a/frontend/src/lib/api/dashboard/reference_data/customs_sections.ts +++ b/frontend/src/lib/api/dashboard/reference_data/customs_sections.ts @@ -31,14 +31,18 @@ export interface UpdateCustomsSectionData { */ export const customsSectionsApi = { /** - * Lista todas las secciones aduaneras con paginación + * Lista todas las secciones aduaneras con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - `/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene una sección aduanera por código diff --git a/frontend/src/lib/api/dashboard/reference_data/customs_warehouses.ts b/frontend/src/lib/api/dashboard/reference_data/customs_warehouses.ts index eae2b955..4e90751e 100644 --- a/frontend/src/lib/api/dashboard/reference_data/customs_warehouses.ts +++ b/frontend/src/lib/api/dashboard/reference_data/customs_warehouses.ts @@ -34,15 +34,18 @@ export interface UpdateCustomsWarehouseData { */ export const customsWarehousesApi = { /** - * Lista todos los recintos fiscalizados con paginación + * Lista todos los recintos fiscalizados con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un recinto fiscalizado por clave compuesta (key + customs) diff --git a/frontend/src/lib/api/dashboard/reference_data/incoterms.ts b/frontend/src/lib/api/dashboard/reference_data/incoterms.ts index 2543824e..4e49deb1 100644 --- a/frontend/src/lib/api/dashboard/reference_data/incoterms.ts +++ b/frontend/src/lib/api/dashboard/reference_data/incoterms.ts @@ -34,15 +34,18 @@ export interface UpdateIncotermData { */ export const incotermsApi = { /** - * Lista todos los incoterms con paginación + * Lista todos los incoterms con paginación y filtros * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param code - Filtrar por clave (opcional) + * @param description - Filtrar por descripción (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, code?: string, description?: string) => { + let url = `/v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`; + if (code) url += `&code=${encodeURIComponent(code)}`; + if (description) url += `&description=${encodeURIComponent(description)}`; + return api.get(url); + }, /** * Obtiene un incoterm por código diff --git a/frontend/src/lib/api/dashboard/reference_data/invoice_types.ts b/frontend/src/lib/api/dashboard/reference_data/invoice_types.ts index 87a7c264..91a90ba8 100644 --- a/frontend/src/lib/api/dashboard/reference_data/invoice_types.ts +++ b/frontend/src/lib/api/dashboard/reference_data/invoice_types.ts @@ -38,12 +38,13 @@ export interface UpdateInvoiceTypeData { */ export const invoiceTypesApi = { /** - * Lista todos los tipos de factura con paginación + * Lista todos los tipos de factura con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) * @param operation - Filtrar por tipo de operación (imp, exp) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50, operation?: string) => { + list: (page = 1, pageSize = 50, operation?: string, search?: string) => { const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString() @@ -51,8 +52,10 @@ export const invoiceTypesApi = { if (operation) { params.append('operation', operation); } + if (search) { + params.append('search', search); + } return api.get( - // CORREGIDO: Añadido '/' antes del '?' `/v1/public/reference_data/invoice-types/?${params.toString()}` ); }, diff --git a/frontend/src/lib/api/dashboard/reference_data/material_types.ts b/frontend/src/lib/api/dashboard/reference_data/material_types.ts index d2db1ab3..a4d89267 100644 --- a/frontend/src/lib/api/dashboard/reference_data/material_types.ts +++ b/frontend/src/lib/api/dashboard/reference_data/material_types.ts @@ -34,12 +34,13 @@ */ export const materialTypesApi = { /** - * Lista todos los tipos de material con paginación + * Lista todos los tipos de material con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) * @param type - Filtrar por tipo (ACTIVO FIJO, MATERIALES, PRODUCTOS) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50, type?: string) => { + list: (page = 1, pageSize = 50, type?: string, search?: string) => { const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString() @@ -47,8 +48,10 @@ export const materialTypesApi = { if (type) { params.append('type', type); } + if (search) { + params.append('search', search); + } return api.get( - // CORRECTO: Ya tiene el '/' antes del '?' `/v1/public/reference_data/material-types/?${params.toString()}` ); }, diff --git a/frontend/src/lib/api/dashboard/reference_data/payment_methods.ts b/frontend/src/lib/api/dashboard/reference_data/payment_methods.ts index e0bcbcaf..7253e13b 100644 --- a/frontend/src/lib/api/dashboard/reference_data/payment_methods.ts +++ b/frontend/src/lib/api/dashboard/reference_data/payment_methods.ts @@ -31,15 +31,18 @@ export interface UpdatePaymentMethodData { */ export const paymentMethodsApi = { /** - * Lista todos los métodos de pago con paginación + * Lista todos los métodos de pago con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un método de pago por key diff --git a/frontend/src/lib/api/dashboard/reference_data/pedimento_codes.ts b/frontend/src/lib/api/dashboard/reference_data/pedimento_codes.ts index 6a5cb837..5d62d34f 100644 --- a/frontend/src/lib/api/dashboard/reference_data/pedimento_codes.ts +++ b/frontend/src/lib/api/dashboard/reference_data/pedimento_codes.ts @@ -31,15 +31,18 @@ export interface UpdatePedimentoCodeData { */ export const pedimentoCodesApi = { /** - * Lista todas las claves de pedimento con paginación + * Lista todas las claves de pedimento con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene una clave de pedimento por code diff --git a/frontend/src/lib/api/dashboard/reference_data/pedimento_regimens.ts b/frontend/src/lib/api/dashboard/reference_data/pedimento_regimens.ts index 9466a70d..124c9a7b 100644 --- a/frontend/src/lib/api/dashboard/reference_data/pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/reference_data/pedimento_regimens.ts @@ -31,15 +31,18 @@ export interface UpdatePedimentoRegimenData { */ export const pedimentoRegimensApi = { /** - * Lista todos los regímenes de pedimento con paginación + * Lista todos los regímenes de pedimento con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un régimen de pedimento por code diff --git a/frontend/src/lib/api/dashboard/reference_data/states.ts b/frontend/src/lib/api/dashboard/reference_data/states.ts index 93a1db35..f7baf99d 100644 --- a/frontend/src/lib/api/dashboard/reference_data/states.ts +++ b/frontend/src/lib/api/dashboard/reference_data/states.ts @@ -37,15 +37,18 @@ export interface UpdateStateData { */ export const statesApi = { /** - * Lista todos los estados con paginación + * Lista todos los estados con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes de '?' - `/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un estado por m3_key diff --git a/frontend/src/lib/api/dashboard/reference_data/transport_modes.ts b/frontend/src/lib/api/dashboard/reference_data/transport_modes.ts index 5bb23ad9..a16b9a4e 100644 --- a/frontend/src/lib/api/dashboard/reference_data/transport_modes.ts +++ b/frontend/src/lib/api/dashboard/reference_data/transport_modes.ts @@ -31,14 +31,18 @@ export interface UpdateTransportModeData { */ export const transportModesApi = { /** - * Lista todos los modos de transporte con paginación + * Lista todos los modos de transporte con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - `/v1/public/reference_data/transport-modes/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/transport-modes/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un modo de transporte por key diff --git a/frontend/src/lib/api/dashboard/reference_data/transport_types.ts b/frontend/src/lib/api/dashboard/reference_data/transport_types.ts index 12b162b1..6b473373 100644 --- a/frontend/src/lib/api/dashboard/reference_data/transport_types.ts +++ b/frontend/src/lib/api/dashboard/reference_data/transport_types.ts @@ -31,12 +31,18 @@ export interface UpdateTransportTypeData { */ export const transportTypesApi = { /** - * Lista todos los tipos de transporte con paginación + * Lista todos los tipos de transporte con paginación y búsqueda + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - `/v1/public/reference_data/transport-types/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/transport-types/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un tipo de transporte por transport_code diff --git a/frontend/src/lib/api/dashboard/reference_data/valuation_methods.ts b/frontend/src/lib/api/dashboard/reference_data/valuation_methods.ts index df4a320c..6f3badb8 100644 --- a/frontend/src/lib/api/dashboard/reference_data/valuation_methods.ts +++ b/frontend/src/lib/api/dashboard/reference_data/valuation_methods.ts @@ -31,15 +31,18 @@ export interface UpdateValuationMethodData { */ export const valuationMethodsApi = { /** - * Lista todos los métodos de valoración con paginación + * Lista todos los métodos de valoración con paginación y búsqueda * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) + * @param search - Término de búsqueda (opcional) */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, search?: string) => { + let url = `/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}`; + if (search) { + url += `&search=${encodeURIComponent(search)}`; + } + return api.get(url); + }, /** * Obtiene un método de valoración por key diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte index 4c448e3d..2f4a88e7 100644 --- a/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte +++ b/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte @@ -60,7 +60,10 @@
-
+
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} diff --git a/frontend/src/lib/components/dashboard/common/infinite-data-table.svelte b/frontend/src/lib/components/dashboard/common/infinite-data-table.svelte new file mode 100644 index 00000000..5fbee4e3 --- /dev/null +++ b/frontend/src/lib/components/dashboard/common/infinite-data-table.svelte @@ -0,0 +1,152 @@ + + +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + {@const headerList = headerGroup.headers} + {@const lastHeaderColId = headerList[headerList.length - 1]?.column.id} + + {#each headerList as header (header.id)} + {@const colId = header.column.id} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + + {/each} + + + {#if table.getRowModel().rows.length} + {#each table.getRowModel().rows as row (row.id)} + {@const visibleCells = row.getVisibleCells()} + {@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id} + + {#each visibleCells as cell (cell.id)} + {@const colId = cell.column.id} + + + + {/each} + + {/each} + {:else} + + + {emptyMessage} + + + {/if} + + {#if hasMore} + + +
+ {#if loading} +
+
+ Cargando más registros... +
+ {:else} +
+ + Desplázate para cargar más + +
+ {/if} +
+
+
+ {/if} +
+
+
+
diff --git a/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte b/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte index 0cdb9644..ec6893aa 100644 --- a/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte +++ b/frontend/src/lib/components/dashboard/export/manifest/data-table.svelte @@ -71,7 +71,10 @@
-
+
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} diff --git a/frontend/src/lib/components/dashboard/general_catalogs/doda/data-table.svelte b/frontend/src/lib/components/dashboard/general_catalogs/doda/data-table.svelte index b96d47d8..483cc79e 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/doda/data-table.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/doda/data-table.svelte @@ -67,14 +67,18 @@ }); -
-
- - +
+
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} - + {#if !header.isPlaceholder} onRowClick?.(row.original)} - class="cursor-pointer transition-colors {row.getIsSelected() - ? 'bg-gray-300 dark:bg-gray-600' - : 'hover:bg-gray-100 dark:hover:bg-gray-700'}" + class="cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}" > {#each row.getVisibleCells() as cell (cell.id)} @@ -103,7 +105,7 @@ {:else} - + No hay resultados. @@ -128,6 +130,7 @@ {/if} - + +
diff --git a/frontend/src/lib/components/dashboard/general_catalogs/inpc/data-table.svelte b/frontend/src/lib/components/dashboard/general_catalogs/inpc/data-table.svelte index 3712b6f3..21ade40a 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/inpc/data-table.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/inpc/data-table.svelte @@ -38,67 +38,69 @@ } -
- - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {#if !header.isPlaceholder} - - {/if} - - {/each} - - {/each} - - - {#if table.getRowModel().rows.length} - {#each table.getRowModel().rows as row (row.id)} - - {#each row.getVisibleCells() as cell (cell.id)} - - - +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + {/each} {/each} - {:else} - - - No hay resultados. - - - {/if} - - -
- -
-
- Total: {totalItems} registros + + + {#if table.getRowModel().rows.length} + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {/each} + {:else} + + + No hay resultados. + + + {/if} + + +
+ +
+
+ Total: {totalItems} registros +
+ +
- -
diff --git a/frontend/src/lib/components/dashboard/general_catalogs/legends/data-table.svelte b/frontend/src/lib/components/dashboard/general_catalogs/legends/data-table.svelte index 72fb9bf3..e1efce5b 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/legends/data-table.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/legends/data-table.svelte @@ -40,67 +40,69 @@ const currentPage = $derived(Number($page.url.searchParams.get('page') || 1)); -
- - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {#if !header.isPlaceholder} - - {/if} - +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + {/each} - - {/each} - - - {#each table.getRowModel().rows as row (row.id)} - - {#each row.getVisibleCells() as cell (cell.id)} - - - + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {:else} + + + No hay resultados. + + {/each} - - {:else} - - - No hay resultados. - - - {/each} - - -
+ + +
-
-
- Total: {totalItems} registros +
+
+ Total: {totalItems} registros +
+
+ + +
+
-
- - -
-
diff --git a/frontend/src/lib/components/dashboard/general_catalogs/sectors/SectorsList.svelte b/frontend/src/lib/components/dashboard/general_catalogs/sectors/SectorsList.svelte index 78137b1f..dd8be92c 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/sectors/SectorsList.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/sectors/SectorsList.svelte @@ -1,9 +1,8 @@ -
+ +
-

{title}

+

{title}

- + -
-
- - +
+ Listado de Sectores +
+
+ + +
- -
+ +
+
- + - Clave - Descripción - Estatus + Clave + Descripción + Estatus @@ -150,7 +154,7 @@ {:else} {#each sectors as sector} - + {sector.key} {sector.description} @@ -174,15 +178,13 @@ {/if} -
- -
-
- Mostrando {sectors.length} de {total} registros
- -
+ +
Mostrando {sectors.length} de {total} registros
+ + +
diff --git a/frontend/src/lib/components/dashboard/goods/fractions/CanadianFractionList.svelte b/frontend/src/lib/components/dashboard/goods/fractions/CanadianFractionList.svelte index 7513162c..17d936e1 100644 --- a/frontend/src/lib/components/dashboard/goods/fractions/CanadianFractionList.svelte +++ b/frontend/src/lib/components/dashboard/goods/fractions/CanadianFractionList.svelte @@ -1,5 +1,5 @@ -
-
-
-
- -
- - +
+ + +
+ Listado de Fracciones Canadienses +
+
+ + +
+
-
- -
- -
- - - - Fracción - Descripción - País - Unidad - ADV - Acciones - - - - {#if fractions.length === 0 && !loading} - - No se encontraron resultados - - {:else} - {#each fractions as fraction} + + +
+
+ + - {fraction.fraction} - {fraction.description || '-'} - {fraction.country_code} - {fraction.unit_of_measure || '-'} - {fraction.ad_valorem ?? '-'} - -
- - -
-
+ Fracción + Descripción + País + Unidad + ADV + Acciones
- {/each} - {/if} - {#if loading} - - -
- -
-
-
- {/if} - -
-
+ + + {#if fractions.length === 0 && !loading} + + No se encontraron resultados + + {:else} + {#each fractions as fraction} + + {fraction.fraction} + {fraction.description || '-'} + {fraction.country_code} + {fraction.unit_of_measure || '-'} + {fraction.ad_valorem ?? '-'} + +
+ + +
+
+
+ {/each} + {/if} + {#if loading} + + +
+ +
+
+
+ {/if} +
+ +
+
+
+ + - -
+
Mostrando {fractions.length} de {totalItems} registros
- import { onMount, untrack } from 'svelte'; + import { untrack } from 'svelte'; import { getHistoricalFractions, deleteHistoricalFraction, @@ -8,23 +8,27 @@ import * as Table from '$lib/components/ui/table'; import { Input } from '$lib/components/ui/input'; import { Button } from '$lib/components/ui/button'; + import * as Card from '$lib/components/ui/card'; import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import HistoricalFractionDialog from './HistoricalFractionDialog.svelte'; - import { companyStore } from '$lib/stores/company.svelte'; +import { companyStore } from '$lib/stores/company.svelte'; - let fractions = $state([]); - let loading = $state(false); - let historicalFraction = $state(''); - let page = $state(1); - let totalItems = $state(0); - let totalPages = $state(0); - let pageSize = 50; +let { title = 'Fracciones históricas' }: { title?: string } = $props(); - let searchTimeout: ReturnType; - let observer: IntersectionObserver; - let sentinel: HTMLDivElement; +let fractions = $state([]); +let loading = $state(false); +let historicalFraction = $state(''); +let page = $state(1); +let totalItems = $state(0); +let totalPages = $state(0); +let pageSize = 50; + +let searchTimeout: ReturnType; +let observer: IntersectionObserver; +let sentinel: HTMLDivElement; +let scrollContainer: HTMLDivElement; // Infinite scroll state let hasMore = $state(true); @@ -138,7 +142,10 @@ loadFractions(false); } }, - { rootMargin: '100px' } + { + root: scrollContainer || null, + rootMargin: '100px' + } ); if (sentinel) observer.observe(sentinel); @@ -163,117 +170,116 @@ }); -
-
-
-
- -
- - -
-
+ +
+ +
+
+

{title}

+

+ Gestiona las fracciones históricas de la tarifa. +

-
-
- - - - Fracción - Tipo - UM - País - Fecha Pub. - Fecha Fin - IGI - IGE - Acciones - - - - {#if fractions.length === 0 && !loading} - - No se encontraron resultados - - {:else} - {#each fractions as fraction} - - {fraction.historical_fraction} - {fraction.fraction_type || '-'} - {fraction.unit_of_measure_code || '-'} - {fraction.country || '-'} - {fraction.publication_date - ? new Date(fraction.publication_date).toLocaleDateString() - : '-'} - {fraction.end_date - ? new Date(fraction.end_date).toLocaleDateString() - : '-'} - {fraction.import_tax_rate ?? '-'} - {fraction.export_tax_rate ?? '-'} - -
- - -
-
-
- {/each} - {/if} - {#if loading} - - -
- -
-
-
- {/if} -
-
-
+ + +
+ Listado de Fracciones Históricas +
+
+ + +
+
+
+
+ +
+
+ + + + Fracción + Tipo + UM + País + Fecha Pub. + Fecha Fin + IGI + IGE + Acciones + + + + {#if fractions.length === 0 && !loading} + + No se encontraron resultados + + {:else} + {#each fractions as fraction} + + {fraction.historical_fraction} + {fraction.fraction_type || '-'} + {fraction.unit_of_measure_code || '-'} + {fraction.country || '-'} + {fraction.publication_date ? new Date(fraction.publication_date).toLocaleDateString() : '-'} + {fraction.end_date ? new Date(fraction.end_date).toLocaleDateString() : '-'} + {fraction.import_tax_rate ?? '-'} + {fraction.export_tax_rate ?? '-'} + +
+ + +
+
+
+ {/each} + {/if} + {#if loading} + + +
+ +
+
+
+ {/if} +
+
+
+
+
+
+
- -
+
Mostrando {fractions.length} de {totalItems} registros
import { Input } from '$lib/components/ui/input'; import { Button } from '$lib/components/ui/button'; - import { Label } from '$lib/components/ui/label'; + import * as Card from '$lib/components/ui/card'; import { Table, TableBody, @@ -17,7 +17,7 @@ type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions'; import { companyStore } from '$lib/stores/company.svelte'; - import { onMount, untrack } from 'svelte'; + import { untrack } from 'svelte'; import * as AlertDialog from '$lib/components/ui/alert-dialog'; import TariffFractionFormDialog from './TariffFractionFormDialog.svelte'; import { toast } from 'svelte-sonner'; @@ -43,6 +43,7 @@ let searchTimeout: ReturnType; let observer: IntersectionObserver; let sentinel: HTMLDivElement; + let scrollContainer: HTMLDivElement; // Infinite scroll state let hasMore = $state(true); @@ -121,7 +122,7 @@ handlePageChange(currentPage + 1); } }, - { rootMargin: '100px' } + { root: scrollContainer || null, rootMargin: '100px' } ); if (sentinel) observer.observe(sentinel); @@ -180,108 +181,118 @@ }); -
+
-

{title}

+

{title}

{#if !readOnly} - {/if}
-
-
- - -
-
- -
- - - - Clave - Fracción - Descripción - {#if catalog === 'mex'} - NICO - U.M.T - {:else} - Unidad - {/if} - Adv. Impo - Adv. Expo - {#if !readOnly} - Acciones - {/if} - - - - {#if fractions.length === 0 && !isLoading} - - - No se encontraron resultados - - - {:else} - {#each fractions as fraction} + + +
+ Listado de Fracciones +
+
+ + +
+
+
+
+ +
+
+
+ - {fraction.um_code || fraction.code} - {fraction.fraction} - - {fraction.description} - + Clave + Fracción + Descripción {#if catalog === 'mex'} - {fraction.nico || '-'} - {fraction.umt || '-'} + NICO + U.M.T {:else} - {fraction.umt || '-'} + Unidad {/if} - {fraction.adv_impo || '-'} - {fraction.adv_expo || '-'} + Adv. Impo + Adv. Expo {#if !readOnly} - -
- - -
-
+ Acciones {/if}
- {/each} - {/if} - {#if isLoading} - - -
- -
-
-
- {/if} - -
-
+ + + {#if fractions.length === 0 && !isLoading} + + + No se encontraron resultados + + + {:else} + {#each fractions as fraction} + + {fraction.um_code || fraction.code} + {fraction.fraction} + + {fraction.description} + + {#if catalog === 'mex'} + {fraction.nico || '-'} + {fraction.umt || '-'} + {:else} + {fraction.umt || '-'} + {/if} + {fraction.adv_impo || '-'} + {fraction.adv_expo || '-'} + {#if !readOnly} + +
+ + +
+
+ {/if} +
+ {/each} + {/if} + {#if isLoading} + + +
+ +
+
+
+ {/if} +
+ +
+
+
+ + - -
+
Mostrando {fractions.length} de {totalFractions} registros
diff --git a/frontend/src/lib/components/dashboard/goods/parts/data-table.svelte b/frontend/src/lib/components/dashboard/goods/parts/data-table.svelte index 2154fe75..6c3212bb 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/data-table.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/data-table.svelte @@ -91,7 +91,10 @@
-
+
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} diff --git a/frontend/src/lib/components/dashboard/invoices/data-table.svelte b/frontend/src/lib/components/dashboard/invoices/data-table.svelte index 870a2c38..3fc036ce 100644 --- a/frontend/src/lib/components/dashboard/invoices/data-table.svelte +++ b/frontend/src/lib/components/dashboard/invoices/data-table.svelte @@ -99,9 +99,13 @@
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {@const headerList = headerGroup.headers} {@const lastHeaderColId = headerList[headerList.length - 1]?.column.id} @@ -111,9 +115,9 @@ onRowClick && onRowClick(row.original)} > {#each visibleCells as cell (cell.id)} @@ -202,12 +204,12 @@ {/if} - + +
diff --git a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte index fa7ee79f..76485457 100644 --- a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte @@ -25,6 +25,9 @@ let pollingInterval: any = null; let isComplete = false; let hasError = false; + let lastResult: any = null; + let externalBody: any = null; + let externalErrors: any[] | null = null; // Reiniciar estado cuando se abre el diálogo con un nuevo taskId $: if (open && taskId) { @@ -32,11 +35,18 @@ statusMessage = 'Iniciando...'; isComplete = false; hasError = false; + lastResult = null; + externalBody = null; + externalErrors = null; startPolling(); } else if (!open) { stopPolling(); } + // Derivar cuerpo y lista de errores del API externo (si existen) + $: externalBody = lastResult?.external_response?.body ?? null; + $: externalErrors = externalBody?.errores ?? null; + function stopPolling() { if (pollingInterval) { clearInterval(pollingInterval); @@ -45,43 +55,44 @@ } async function pollOnce() { - if (!taskId) return; - const apiCall = getStatus || invoicesReportsApi.getTaskStatus; - const raw = await apiCall(taskId); - const response = raw?.data !== undefined ? raw.data : raw; - if (raw?.error) { - hasError = true; - statusMessage = `Error: ${raw.error}`; - stopPolling(); - toast.error(raw.error); - return; - } - if (response?.state === 'PROCESSING' && response.info) { - progress = response.info.current || 0; - statusMessage = response.info.status || 'Procesando...'; - } else if (response?.state === 'SUCCESS') { - const result = response.result; - stopPolling(); - - // Si el worker reportó error de validación o error de aplicación, - // cerrar el dialog inmediatamente y dejar que onComplete muestre el toast. - if (result?.status === 'validation_error' || result?.status === 'error') { - open = false; - onComplete(result); - } else { - progress = 100; - statusMessage = '¡Completado!'; - isComplete = true; - setTimeout(() => onComplete(result), 500); - } - } else if (response?.state === 'FAILURE') { - hasError = true; - const errMsg = response.result ? String(response.result) : 'Error desconocido'; - statusMessage = `Error: ${errMsg}`; - stopPolling(); - toast.error(`Falló: ${errMsg}`); - } + if (!taskId) return; + const apiCall = getStatus || invoicesReportsApi.getTaskStatus; + const raw = await apiCall(taskId); + const response = raw?.data !== undefined ? raw.data : raw; + if (raw?.error) { + hasError = true; + statusMessage = `Error: ${raw.error}`; + stopPolling(); + toast.error(raw.error); + return; } + if (response?.state === 'PROCESSING' && response.info) { + progress = response.info.current || 0; + statusMessage = response.info.status || 'Procesando...'; + } else if (response?.state === 'SUCCESS') { + const result = response.result; + stopPolling(); + lastResult = result; + + if (result?.status === 'validation_error' || result?.status === 'error') { + // Quedarse abierto, marcar error y dejar que onComplete maneje los mensajes + hasError = true; + isComplete = true; + onComplete(result); + } else { + progress = 100; + statusMessage = '¡Completado!'; + isComplete = true; + onComplete(result); + } + } else if (response?.state === 'FAILURE') { + hasError = true; + const errMsg = response.result ? String(response.result) : 'Error desconocido'; + statusMessage = `Error: ${errMsg}`; + stopPolling(); + toast.error(`Falló: ${errMsg}`); + } + } async function startPolling() { stopPolling(); // Asegurar limpieza previa @@ -149,7 +160,14 @@
{/each}
- +
+ +
+ {#if statusMessage} +
+ {statusMessage} +
+ {/if} {:else}
{statusMessage} @@ -160,11 +178,50 @@
{#if isComplete} -
+
- {completeMessage} + + {#if lastResult?.cove_number} + COVE generado + {:else if lastResult?.status === 'validation_error'} + Se encontraron errores de validación + {:else} + {completeMessage} + {/if} + + + {#if lastResult?.cove_number} + + COVE: {lastResult.cove_number} + + {/if} + {#if lastResult?.external_task_id} + + Task ID COVE: {lastResult.external_task_id} + + {:else if taskId} + + Task interno: {taskId} + + {/if} + + + {#if lastResult?.status === 'validation_error' && externalBody} + + {externalBody.mensaje || 'Datos inválidos en el servicio COVE.'} + + {#if externalErrors && externalErrors.length} +
    + {#each externalErrors as e} +
  • {e.campo}: {e.mensaje}
  • + {/each} +
+ {/if} + {:else if lastResult?.message} + + {lastResult.message} + + {/if}
{:else if hasError}
- {#if hasError} - + {#if isComplete || hasError} + {/if} diff --git a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte index f2141ae7..eb51c5d2 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte @@ -97,16 +97,25 @@
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} - + {@const colId = header.column.id} + {#if !header.isPlaceholder}
diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts index 41ae7ce2..065745f5 100644 --- a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts @@ -12,19 +12,6 @@ export type CodePedimentoRegimen = { export function createColumns(onSuccess?: () => void): ColumnDef[] { return [ - { - accessorKey: "id", - header: "ID", - cell: ({ row }) => { - const idSnippet = createRawSnippet<[{ id: number }]>((getId) => { - const { id } = getId(); - return { - render: () => `
${id}
` - }; - }); - return renderSnippet(idSnippet, { id: row.original.id }); - } - }, { accessorKey: "pedimento_code", header: "Código Pedimento", diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte index 18c91f2d..faa561f9 100644 --- a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { CodePedimentoRegimen } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.id.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte index 07f22b3b..9689ba27 100644 --- a/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { Container } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte index cd8ee7db..fce57c19 100644 --- a/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { Country } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.m3_key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table-actions.svelte index 63dd58c3..69d90ade 100644 --- a/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { CurrencyType } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.code.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/currency_types/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table-actions.svelte index 3ffd2ee3..8dcf4a58 100644 --- a/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { CustomsSection } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.customs_code.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table.svelte index 4c448e3d..0e0ae059 100644 --- a/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table-actions.svelte index d204b9c0..3fd6f9fe 100644 --- a/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { CustomsWarehouse } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(`${item.key}|${item.customs}`); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table-actions.svelte index ce9a6cbe..9ea03340 100644 --- a/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { Incoterm } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.code.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table.svelte index 3d5b2a66..c144ded1 100644 --- a/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/incoterms/data-table.svelte @@ -3,104 +3,114 @@ type ColumnDef, getCoreRowModel } from "@tanstack/table-core"; + import { onMount } from "svelte"; import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js"; import * as Table from "$lib/components/ui/table/index.js"; import { Button } from "$lib/components/ui/button"; - import { goto } from "$app/navigation"; - import { page } from "$app/stores"; type DataTableProps = { columns: ColumnDef[]; data: TData[]; - pageCount: number; - totalItems: number; + loading: boolean; + hasMore: boolean; + loadMore: () => void; }; let { data, columns, - pageCount, - totalItems + loading, + hasMore, + loadMore }: DataTableProps = $props(); const table = createSvelteTable({ get data() { return data; }, get columns() { return columns; }, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - get pageCount() { return pageCount; }, + getCoreRowModel: getCoreRowModel() }); - function handlePageChange(newPage: number) { - const url = new URL($page.url); - url.searchParams.set('page', newPage.toString()); - goto(url, { keepFocus: true, noScroll: true }); - } + let scrollContainer = $state(); + let loadingTrigger = $state(); - const currentPage = $derived(Number($page.url.searchParams.get('page') || 1)); + onMount(() => { + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting && hasMore && !loading) { + loadMore(); + } + }, + { root: scrollContainer, threshold: 0.1 } + ); + + if (loadingTrigger) observer.observe(loadingTrigger); + + return () => observer.disconnect(); + }); -
- - - {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {#if !header.isPlaceholder} +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {/each} + + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + {/each} + + {:else} + + + + {/each} + + + {#if hasMore} + + + + {/if} + +
+ {#if !header.isPlaceholder} + + {/if} +
- {/if} - - {/each} - - {/each} - - - {#each table.getRowModel().rows as row (row.id)} - - {#each row.getVisibleCells() as cell (cell.id)} - - - - {/each} - - {:else} - - - No hay resultados. - - - {/each} - - - - -
-
- Total: {totalItems} registros -
-
- - +
+ No hay resultados. +
+
+ {#if loading} +
+
+ Cargando más registros... +
+ {:else} +
+ + Desplázate para cargar más + +
+ {/if} +
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table-actions.svelte index be22d1e5..4fab7cbe 100644 --- a/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { InvoiceType } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/material_types/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/material_types/data-table-actions.svelte index 8b64e467..d1f070f7 100644 --- a/frontend/src/lib/components/dashboard/reference_data/material_types/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/material_types/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { MaterialType } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/material_types/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/material_types/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/material_types/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/material_types/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table-actions.svelte index faea7dde..f36a0c6a 100644 --- a/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { PaymentMethod } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table-actions.svelte index 21b69276..5bfd8144 100644 --- a/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { PedimentoCode } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.code.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table-actions.svelte index 42b87981..dc4d01ad 100644 --- a/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { PedimentoRegimen } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.code.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte index 1da14dbf..460e1202 100644 --- a/frontend/src/lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte @@ -25,6 +25,19 @@ let loading = $state(false); let error = $state(null); + function getActiveCompanyId(): number | null { + const fromStore = companyStore.activeCompany?.id; + if (fromStore) return fromStore; + if (typeof document === 'undefined') return null; + const cookie = document.cookie + .split('; ') + .find((row) => row.startsWith('active_company_id=')) + ?.split('=')[1]; + if (!cookie) return null; + const parsed = Number(cookie); + return Number.isFinite(parsed) ? parsed : null; + } + // Actualizar formData cuando item cambia $effect(() => { if (item) { @@ -49,7 +62,7 @@ loading = true; error = null; - const companyId = companyStore.activeCompany?.id; + const companyId = getActiveCompanyId(); if (!companyId) { error = 'No hay empresa activa seleccionada'; loading = false; diff --git a/frontend/src/lib/components/dashboard/reference_data/sectors/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/sectors/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/sectors/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/sectors/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/sectors/delete-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/sectors/delete-dialog.svelte index 9e92fb82..920913e9 100644 --- a/frontend/src/lib/components/dashboard/reference_data/sectors/delete-dialog.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/sectors/delete-dialog.svelte @@ -18,10 +18,23 @@ let loading = $state(false); let error = $state(null); + function getActiveCompanyId(): number | null { + const fromStore = companyStore.activeCompany?.id; + if (fromStore) return fromStore; + if (typeof document === 'undefined') return null; + const cookie = document.cookie + .split('; ') + .find((row) => row.startsWith('active_company_id=')) + ?.split('=')[1]; + if (!cookie) return null; + const parsed = Number(cookie); + return Number.isFinite(parsed) ? parsed : null; + } + async function handleDelete() { if (!item) return; - const companyId = companyStore.activeCompany?.id; + const companyId = getActiveCompanyId(); if (!companyId) { error = 'No hay empresa activa seleccionada'; return; diff --git a/frontend/src/lib/components/dashboard/reference_data/states/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/states/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/states/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/states/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table-actions.svelte index 9159af82..5855ea10 100644 --- a/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { TransportMode } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table-actions.svelte index 6c4f2e5b..de253afb 100644 --- a/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { TransportType } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.transport_code.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table.svelte index 4c448e3d..1e44175d 100644 --- a/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/transport_types/data-table.svelte @@ -59,65 +59,67 @@ }); -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table-actions.svelte index cf14da9a..f4bf003f 100644 --- a/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table-actions.svelte @@ -3,9 +3,7 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { ValuationMethod } from "./columns.js"; - import CreateEditDialog from "./create-edit-dialog.svelte"; import DetailsDialog from "./details-dialog.svelte"; - import DeleteDialog from "./delete-dialog.svelte"; let { item, @@ -16,8 +14,6 @@ } = $props(); let showDetailsDialog = $state(false); - let showEditDialog = $state(false); - let showDeleteDialog = $state(false); function handleCopyId() { navigator.clipboard.writeText(item.key.toString()); @@ -26,14 +22,6 @@ function handleViewDetails() { showDetailsDialog = true; } - - function handleEdit() { - showEditDialog = true; - } - - function handleDelete() { - showDeleteDialog = true; - } @@ -54,13 +42,8 @@ Ver detalles - Editar - - Eliminar - - diff --git a/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table.svelte index 4c84570d..17039f91 100644 --- a/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table.svelte @@ -60,65 +60,67 @@ -
-
- - +
+
+ + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} - + {#each headerGroup.headers as header (header.id)} - + {/each} - + {/each} - - + + {#each table.getRowModel().rows as row (row.id)} - + {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - + {:else} - - + + + {/each} - + {#if hasMore} - - -
+
+ + {/if} - - + +
{#if !header.isPlaceholder} {/if} - +
- +
No hay resultados. - - +
+
{#if loading} -
-
- Cargando más... +
+
+ Cargando más registros...
{:else} -
- Desplázate para cargar más +
+ + Desplázate para cargar más +
{/if}
- - +
diff --git a/frontend/src/lib/components/dashboard/seal/data-table.svelte b/frontend/src/lib/components/dashboard/seal/data-table.svelte index 207266e0..954785f8 100644 --- a/frontend/src/lib/components/dashboard/seal/data-table.svelte +++ b/frontend/src/lib/components/dashboard/seal/data-table.svelte @@ -18,8 +18,8 @@ let { data, columns, loading = false, hasMore = false, loadMore }: Props = $props(); - let scrollContainer: HTMLDivElement; - let observer: IntersectionObserver; + let scrollContainer = $state(); + let loadingTrigger = $state(); let options = $derived>({ get data() { @@ -34,8 +34,7 @@ onMount(() => { if (!loadMore) return; - // Create intersection observer for infinite scroll - observer = new IntersectionObserver( + const observer = new IntersectionObserver( (entries) => { const [entry] = entries; if (entry.isIntersecting && hasMore && !loading && loadMore) { @@ -48,26 +47,24 @@ } ); - // Observe the last row - const lastRow = scrollContainer?.querySelector('tbody tr:last-child'); - if (lastRow) { - observer.observe(lastRow); + if (loadingTrigger) { + observer.observe(loadingTrigger); } return () => { - observer?.disconnect(); + observer.disconnect(); }; }); -
-
+
+
- + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} - + {#if !header.isPlaceholder} {#each row.getVisibleCells() as cell (cell.id)} - + {/each} - - {#if loading} + + {#if hasMore} - - Cargando... + +
+ {#if loading} +
+
+ Cargando más registros... +
+ {:else} +
+ + Desplázate para cargar más + +
+ {/if} +
{/if} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte index d862761d..5d9598f5 100644 --- a/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte +++ b/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -14,7 +14,7 @@ bind:this={ref} data-slot="sidebar-inset" class={cn( - "bg-background relative flex w-full flex-1 flex-col", + "bg-background relative flex min-h-0 w-full flex-1 flex-col overflow-hidden", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm", className )} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte index 5b0d0aa2..f9f8f9dc 100644 --- a/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte +++ b/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -42,7 +42,7 @@ data-slot="sidebar-wrapper" style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}" class={cn( - "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", + "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex h-svh w-full overflow-hidden", className )} bind:this={ref} diff --git a/frontend/src/lib/config/shortcuts/dashboard/reference_data/common/factory.ts b/frontend/src/lib/config/shortcuts/dashboard/reference_data/common/factory.ts index 562c133e..6a487710 100644 --- a/frontend/src/lib/config/shortcuts/dashboard/reference_data/common/factory.ts +++ b/frontend/src/lib/config/shortcuts/dashboard/reference_data/common/factory.ts @@ -4,19 +4,24 @@ import type { ShortcutDef } from '$lib/stores/shortcut-store'; * Reusable shortcuts for simple reference data catalogs. */ export const obtenerAtajosReferenceDataSimple = (acciones: { - manejarNuevo: () => void; + manejarNuevo?: () => void; manejarActualizar: () => void; }): ShortcutDef[] => { - return [ - { + const atajos: ShortcutDef[] = []; + + if (acciones.manejarNuevo) { + atajos.push({ key: 'Alt+Shift+N', description: 'Nuevo Registro', action: acciones.manejarNuevo - }, - { - key: 'Alt+Shift+R', - description: 'Actualizar Lista', - action: acciones.manejarActualizar - } - ]; + }); + } + + atajos.push({ + key: 'Alt+Shift+R', + description: 'Actualizar Lista', + action: acciones.manejarActualizar + }); + + return atajos; }; diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 12e28e3f..b9cca54b 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -141,7 +141,7 @@ -->
-
+
{#if csvImportBanner} -
+

{m['sidebar.audit_logs_title']()}

diff --git a/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte b/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte index da1e2bd7..0269e9e3 100644 --- a/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte +++ b/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte @@ -8,7 +8,6 @@ import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Label } from '$lib/components/ui/label'; import * as Table from '$lib/components/ui/table'; import { RefreshCw, Search } from 'lucide-svelte'; @@ -153,106 +152,78 @@
-
- -
- -
- - - Filtros - - -
-
- -
- - -
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- -
-
-
-
-
- Registros - Total: {total} registros encontrados + Bitácora de Auditoría + Mostrando {logs.length} de {total} registros +
+
+
+ + +
+ + + + - + + +
diff --git a/frontend/src/routes/dashboard/audit_logs/files-tab.svelte b/frontend/src/routes/dashboard/audit_logs/files-tab.svelte index 051df209..fc652973 100644 --- a/frontend/src/routes/dashboard/audit_logs/files-tab.svelte +++ b/frontend/src/routes/dashboard/audit_logs/files-tab.svelte @@ -80,58 +80,53 @@
-
-
-

{m['sidebar.audit_logs_files_title']()}

-

{displayPath || m['sidebar.audit_logs_files_root']()}

-
- -
- - - -
- {#each breadcrumbs as crumb, idx} - {#if idx > 0} - / - {/if} - {#if idx === breadcrumbs.length - 1} - - {crumb.display_name} - - {:else} - - {/if} - {/each} -
-
-
- - - {m['sidebar.audit_logs_files_list_title']()} + +
+
+ {m['sidebar.audit_logs_files_title']()} + +
+ {#each breadcrumbs as crumb, idx} + {#if idx > 0} + / + {/if} + {#if idx === breadcrumbs.length - 1} + + {crumb.display_name} + + {:else} + + {/if} + {/each} + {#if breadcrumbs.length === 0} + {m['sidebar.audit_logs_files_root']()} + {/if} +
+
+
+ +
- + {#if error} -
+
{m['sidebar.audit_logs_files_error_prefix']()} {error}
{/if} -
+
- + {m['sidebar.audit_logs_files_col_name']()} @@ -163,7 +158,7 @@ {:else} {#each folders as folder} void loadPath(folder.path)} > @@ -178,7 +173,7 @@ {/each} {#each files as file} - + {file.display_name} diff --git a/frontend/src/routes/dashboard/audit_logs/tasks-tab.svelte b/frontend/src/routes/dashboard/audit_logs/tasks-tab.svelte index 957e4c1e..e81ba92b 100644 --- a/frontend/src/routes/dashboard/audit_logs/tasks-tab.svelte +++ b/frontend/src/routes/dashboard/audit_logs/tasks-tab.svelte @@ -6,9 +6,9 @@ type UnifiedTaskStatus } from '$lib/api/dashboard/a76/tasks'; import { companyStore } from '$lib/stores/company.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import * as Select from '$lib/components/ui/select'; import * as Dialog from '$lib/components/ui/dialog'; import { Badge } from '$lib/components/ui/badge'; @@ -146,106 +146,116 @@ }); -
-
- -
+
+ + +
+
+ Tareas del Sistema + Total: {total} tareas encontradas +
+
+ + + + +
+
+
+ + {#if error} +
+ {error} +
+ {/if} -
- - - - - Todos - En cola - En progreso - Completadas - Fallidas - - - -
- - {#if error} -
- {error} -
- {/if} - -
- - - - - - - - - - - - - {#if tasks.length === 0 && !loading} - - - - {:else if tasks.length === 0 && loading} - - - - {:else} - {#each tasks as task} - void openDetail(task)} - > - - - - - - +
+
Task IDTipoEstadoProgresoReintentosActualizado
Sin tareas registradas
Cargando...
{task.task_id}{task.task_group} / {task.task_name} - {statusLabel(task.status)} - ({task.celery_state_raw}) - - {formatPercent(task)} - {task.progress?.message ? ` · ${task.progress.message}` : ''} - {task.retries ?? 0}{new Date(task.updated_at).toLocaleString()}
+ + + + + + + + - {/each} - {/if} - -
Task IDTipoEstadoProgresoReintentosActualizado
-
- -
-
Total: {total}
-
- - Página {page} - + + + {#if tasks.length === 0 && !loading} + + Sin tareas registradas + + {:else if tasks.length === 0 && loading} + + Cargando... + + {:else} + {#each tasks as task} + void openDetail(task)} + > + {task.task_id} + {task.task_group} / {task.task_name} + + {statusLabel(task.status)} + ({task.celery_state_raw}) + + + {formatPercent(task)} + {task.progress?.message ? ` · ${task.progress.message}` : ''} + + {task.retries ?? 0} + {new Date(task.updated_at).toLocaleString()} + + {/each} + {/if} + + +
+ +
+
Total: {total}
+
+ + Página {page} + +
-
+
-
-
-
-

GESTIÓN ADUANAL

-

Administración de Agentes y Secciones Aduanales

+
+
+
+

Gestión Aduanal

+

Administración de Agentes y Secciones Aduanales

- + -
-
+
+

Filtros

@@ -228,7 +228,7 @@
@@ -237,7 +237,7 @@
@@ -246,8 +246,8 @@
-
-
+
+

Listado

@@ -260,7 +260,7 @@
-
+
-
+

Detalles del Agente @@ -407,6 +405,8 @@ + +

import { page } from '$app/stores'; - import DataTable from '$lib/components/dashboard/general_catalogs/classification_concepts/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { createColumns } from '$lib/components/dashboard/general_catalogs/classification_concepts/columns'; import CreateDialog from '$lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaClasificacion } from '$lib/config/shortcuts/dashboard/general_catalogs/classification/list'; + import { getClassificationConcepts } from '$lib/api/dashboard/a76/general_catalogs/classification-concepts'; + import { companyStore } from '$lib/stores/company.svelte'; let { data } = $props(); let dialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -28,17 +32,54 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); - } - const columns = $derived(createColumns(handleSuccess)); - function handleSearch() { + let allItems = $state(data.classifications?.items || []); + let currentPage = $state(data.classifications?.page || 1); + let pageSize = $state(data.classifications?.page_size || 50); + let totalItems = $state(data.classifications?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.classifications) { + allItems = data.classifications.items || []; + currentPage = data.classifications.page || 1; + totalItems = data.classifications.total || 0; + pageSize = data.classifications.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchClassification) filters.classification = searchClassification; + if (searchDesc) filters.description = searchDesc; + + const response = await getClassificationConcepts( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchClassification) url.searchParams.set('classification', searchClassification); else url.searchParams.delete('classification'); @@ -46,47 +87,131 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchClassification) filters.classification = searchClassification; + if (searchDesc) filters.description = searchDesc; + + const response = await getClassificationConcepts( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more classifications:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchClassification) filters.classification = searchClassification; + if (searchDesc) filters.description = searchDesc; + + const response = await getClassificationConcepts( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading classifications:', err); + } finally { + loading = false; + } + } + + function handleSuccess() { + reloadData(); + } -
+

Clasificaciones de Conceptos

Gestión del catálogo de clasificaciones de conceptos

- -
- -
-
- -
-
- +
+ +
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Clasificaciones +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros +
import { page } from '$app/stores'; - import DataTable from '$lib/components/dashboard/general_catalogs/company/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import { createColumns } from '$lib/components/dashboard/general_catalogs/company/columns'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import * as m from '$lib/paraglide/messages.js'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaEmpresa } from '$lib/config/shortcuts/dashboard/general_catalogs/company_information/list'; + import { getCompanies } from '$lib/api/dashboard/a76/general_catalogs/company'; let { data } = $props(); @@ -24,16 +26,53 @@ }) ); let dialogOpen = $state(false); + let error = $state(data.error || null); // Filtros let searchName = $state($page.url.searchParams.get('name') || ''); let searchRfc = $state($page.url.searchParams.get('rfc') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.companies?.items || []); + let currentPage = $state(data.companies?.page || 1); + let pageSize = $state(data.companies?.page_size || 50); + let totalItems = $state(data.companies?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.companies) { + allItems = data.companies.items || []; + currentPage = data.companies.page || 1; + totalItems = data.companies.total || 0; + pageSize = data.companies.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchName) filters.name = searchName; + if (searchRfc) filters.rfc = searchRfc; + + const response = await getCompanies(1, pageSize, filters); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchName) url.searchParams.set('name', searchName); else url.searchParams.delete('name'); @@ -41,44 +80,118 @@ if (searchRfc) url.searchParams.set('rfc', searchRfc); else url.searchParams.delete('rfc'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchName) filters.name = searchName; + if (searchRfc) filters.rfc = searchRfc; + + const response = await getCompanies(currentPage + 1, pageSize, filters); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more companies:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchName) filters.name = searchName; + if (searchRfc) filters.rfc = searchRfc; + + const response = await getCompanies(1, pageSize, filters); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading companies:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Información de Empresas

Gestión de información de empresas

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Empresas +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte index 1401d421..d4c5d42c 100644 --- a/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte @@ -466,15 +466,16 @@ if (!file) return; - // Validar extensión + // Validar extensión de forma estricta según el tipo de campo: + // - *_cer => solo .cer + // - *_key => solo .key const ext = file.name.split('.').pop()?.toLowerCase(); - let allowed = ['cer', 'key']; - if (type.includes('cer') && ext !== 'cer') { + if (type.toLowerCase().endsWith('_cer') && ext !== 'cer') { error = 'El archivo debe ser .cer'; target.value = ''; // Reset return; } - if (type.includes('key') && ext !== 'key') { + if (type.toLowerCase().endsWith('_key') && ext !== 'key') { error = 'El archivo debe ser .key'; target.value = ''; // Reset return; diff --git a/frontend/src/routes/dashboard/general_catalogs/concepts/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/concepts/+page.svelte index 63507986..33c48136 100644 --- a/frontend/src/routes/dashboard/general_catalogs/concepts/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/concepts/+page.svelte @@ -1,21 +1,25 @@ -
+

Conceptos

Gestión del catálogo de conceptos

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Conceptos +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
{#if dialogOpen} diff --git a/frontend/src/routes/dashboard/general_catalogs/customs_broker_concepts/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/customs_broker_concepts/+page.svelte index 0f19a0c1..a049231d 100644 --- a/frontend/src/routes/dashboard/general_catalogs/customs_broker_concepts/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/customs_broker_concepts/+page.svelte @@ -1,21 +1,24 @@ -
+

Conceptos de Agente Aduanal

Gestión del catálogo de conceptos de agente aduanal

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Conceptos de Agente Aduanal +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
{#if activeCompanyId} diff --git a/frontend/src/routes/dashboard/general_catalogs/doda/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/doda/+page.svelte index 80684dce..258c3647 100644 --- a/frontend/src/routes/dashboard/general_catalogs/doda/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/doda/+page.svelte @@ -3,7 +3,6 @@ import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import { getDodas, deleteDoda, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda'; - import * as Card from '$lib/components/ui/card'; import { toast } from 'svelte-sonner'; import { Plus, @@ -16,6 +15,7 @@ LayoutGrid, Printer } from 'lucide-svelte'; + import * as Card from '$lib/components/ui/card'; import * as Select from '$lib/components/ui/select'; import { companyStore } from '$lib/stores/company.svelte'; import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte'; @@ -23,7 +23,6 @@ import CreateEditDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Label } from '$lib/components/ui/label'; import { Separator } from '$lib/components/ui/separator'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list'; @@ -194,60 +193,45 @@ } -
-
-
-

DODA

+
+
+
+

DODA

Gestiona tus Documentos de Operación Aduanera (DODA)

- +
+ + +
- + -
-
- Filtros Avanzados - Refina tu búsqueda mediante múltiples criterios -
- -
-
- -
-
- +
+ Listado de DODA +
- +
-
- -
- - -
- -
- + (filters.status = v)} > - - {filters.status || 'Todos los estatus'} + + {filters.status || 'Estatus'} Todos @@ -257,21 +241,17 @@ ELIMINADO -
- -
- (filters.operation_type = v)} > - + {filters.operation_type === 'I' ? 'Importación' : filters.operation_type === 'E' ? 'Exportación' - : 'Todas'} + : 'Operación'} Todas @@ -279,38 +259,35 @@ E - Exportación +
+ + +
+ +
- - -
-
- Listado de DODAs - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - -
+
+ Mostrando {allItems.length} de {totalItems} registros + + Filtros activos: {Object.values(filters).filter((value) => value !== '').length} +
+ +
-
- - {#if dialogOpen} {/if} diff --git a/frontend/src/routes/dashboard/general_catalogs/doda/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/doda/edit/[[id]]/+page.svelte index 67e241a0..db7208ec 100644 --- a/frontend/src/routes/dashboard/general_catalogs/doda/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/doda/edit/[[id]]/+page.svelte @@ -262,7 +262,7 @@ > -

{title}

+

{title}

Catálogos Generales / Doda

diff --git a/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte index 03a29c1c..0f089351 100644 --- a/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte @@ -1,11 +1,12 @@ -
+
-

Conductores

+

Conductores

Gestión del catálogo de conductores

- +
-
- - -
+ +
Listado de Conductores
+ {#if loading && data.length === 0}
Cargando conductores...
{:else}
{/if}
+
- {#if loading && data.length === 0} -
- Cargando conductores... -
- {:else} - - {/if} +
Mostrando {data.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/electronic_notices/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/electronic_notices/+page.svelte index 3c3fa9f7..7aa48435 100644 --- a/frontend/src/routes/dashboard/general_catalogs/electronic_notices/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/electronic_notices/+page.svelte @@ -2,17 +2,21 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { browser } from '$app/environment'; - import DataTable from '$lib/components/dashboard/general_catalogs/electronic_notices/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic_notices/columns.js'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic_notices/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaAvisosElectrónicos } from '$lib/config/shortcuts/dashboard/general_catalogs/electronic_notices/list'; + import { getElectronicNotices } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices'; + import { companyStore } from '$lib/stores/company.svelte'; let { data } = $props(); let dialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -28,10 +32,52 @@ let searchPedimento = $state($page.url.searchParams.get('pedimento') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.notices?.items || []); + let currentPage = $state(data.notices?.page || 1); + let pageSize = $state(data.notices?.page_size || 50); + let totalItems = $state(data.notices?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.notices) { + allItems = data.notices.items || []; + currentPage = data.notices.page || 1; + totalItems = data.notices.total || 0; + pageSize = data.notices.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchNotice) filters.notice_number = searchNotice; + if (searchPedimento) filters.pedimento = searchPedimento; + + const response = await getElectronicNotices( + 1, + pageSize, + filters, + companyStore.activeCompany.id + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchNotice) url.searchParams.set('notice_number', searchNotice); else url.searchParams.delete('notice_number'); @@ -39,54 +85,92 @@ if (searchPedimento) url.searchParams.set('pedimento', searchPedimento); else url.searchParams.delete('pedimento'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchNotice) filters.notice_number = searchNotice; + if (searchPedimento) filters.pedimento = searchPedimento; + + const response = await getElectronicNotices( + currentPage + 1, + pageSize, + filters, + companyStore.activeCompany.id + ); + if (response?.items) { + allItems = [...allItems, ...response.items]; + currentPage += 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more electronic notices:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchNotice) filters.notice_number = searchNotice; + if (searchPedimento) filters.pedimento = searchPedimento; + + const response = await getElectronicNotices( + 1, + pageSize, + filters, + companyStore.activeCompany.id + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading electronic notices:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Avisos Electrónicos

Gestión del catálogo de avisos electrónicos

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Avisos
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/equivalencies/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/equivalencies/+page.svelte index d86f780d..51a76f07 100644 --- a/frontend/src/routes/dashboard/general_catalogs/equivalencies/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/equivalencies/+page.svelte @@ -4,16 +4,20 @@ import { browser } from '$app/environment'; import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies'; import { createCatalogColumns } from '$lib/components/dashboard/general_catalogs/equivalencies/catalog-columns'; - import DataTable from '$lib/components/dashboard/general_catalogs/equivalencies/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaEquivalencias } from '$lib/config/shortcuts/dashboard/general_catalogs/equivalencies/list'; import DataEquivalenciesDialog from '$lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte'; import type { PageData } from './$types'; + import { getEquivalencies } from '$lib/api/dashboard/a76/general_catalogs/equivalencies'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); + let error = $state(data.error || null); let dataDialogOpen = $state(false); let selectedEquivalency = $state(null); @@ -36,22 +40,115 @@ let searchFrom = $state($page.url.searchParams.get('from_unit_code') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.equivalencies?.items || []); + let currentPage = $state(data.equivalencies?.page || 1); + let pageSize = $state(data.equivalencies?.page_size || 50); + let totalItems = $state(data.equivalencies?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.equivalencies) { + allItems = data.equivalencies.items || []; + currentPage = data.equivalencies.page || 1; + totalItems = data.equivalencies.total || 0; + pageSize = data.equivalencies.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchFrom) filters.from_unit_code = searchFrom; + + const response = await getEquivalencies( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchFrom) url.searchParams.set('from_unit_code', searchFrom); else url.searchParams.delete('from_unit_code'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchFrom) filters.from_unit_code = searchFrom; + + const response = await getEquivalencies( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more equivalencies:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchFrom) filters.from_unit_code = searchFrom; + + const response = await getEquivalencies( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading equivalencies:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } function handleOpenInsertItems(equivalency: Equivalency) { @@ -67,13 +164,13 @@ } -
+

Equivalencias

Catálogo de equivalencias

- +
-
-
- + {#if error} +
+ {error}
-
+ {/if} - + +
Listado de Equivalencias
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
(data.error || null); // Atajos useShortcuts( @@ -29,10 +33,52 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.errors?.items || []); + let currentPage = $state(data.errors?.page || 1); + let pageSize = $state(data.errors?.page_size || 50); + let totalItems = $state(data.errors?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.errors) { + allItems = data.errors.items || []; + currentPage = data.errors.page || 1; + totalItems = data.errors.total || 0; + pageSize = data.errors.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getErrorCatalogs( + companyStore.activeCompany.id, + 1, + pageSize, + filters + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -40,50 +86,92 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getErrorCatalogs( + companyStore.activeCompany.id, + currentPage + 1, + pageSize, + filters + ); + if (response?.items) { + allItems = [...allItems, ...response.items]; + currentPage += 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more error catalogs:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getErrorCatalogs( + companyStore.activeCompany.id, + 1, + pageSize, + filters + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading error catalogs:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Errores de Facturación

Gestión del catálogo de errores de facturación

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Errores
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/exchange-rate/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/exchange-rate/+page.svelte index d2c20235..a68435cb 100644 --- a/frontend/src/routes/dashboard/general_catalogs/exchange-rate/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/exchange-rate/+page.svelte @@ -4,17 +4,21 @@ import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/exchange_rate/columns'; import CreateEditDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte'; - import DataTable from '$lib/components/dashboard/exchange_rate/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaTiposCambio } from '$lib/config/shortcuts/dashboard/general_catalogs/exchange_rate/list'; import type { PageData } from './$types'; + import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -36,55 +40,127 @@ let searchCurrency = $state($page.url.searchParams.get('local_currency') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.exchange_rates?.items || []); + let currentPage = $state(data.exchange_rates?.page || 1); + let pageSize = $state(data.exchange_rates?.page_size || 50); + let totalItems = $state(data.exchange_rates?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.exchange_rates) { + allItems = data.exchange_rates.items || []; + currentPage = data.exchange_rates.page || 1; + totalItems = data.exchange_rates.total || 0; + pageSize = data.exchange_rates.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const response = await getExchangeRates(companyStore.activeCompany.id, { + local_currency: searchCurrency || undefined, + page: 1, + page_size: pageSize + }); + if (response.data) { + allItems = response.data.items; + currentPage = response.data.page || 1; + totalItems = response.data.total || 0; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCurrency) url.searchParams.set('local_currency', searchCurrency); else url.searchParams.delete('local_currency'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const response = await getExchangeRates(companyStore.activeCompany.id, { + local_currency: searchCurrency || undefined, + page: currentPage + 1, + page_size: pageSize + }); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total || 0; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more exchange rates:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const response = await getExchangeRates(companyStore.activeCompany.id, { + local_currency: searchCurrency || undefined, + page: 1, + page_size: pageSize + }); + if (response.data) { + allItems = response.data.items; + currentPage = response.data.page || 1; + totalItems = response.data.total || 0; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading exchange rates:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Tipos de Cambio

Catálogo de tipos de cambio

- +
-
-
- + {#if error} +
+ {error}
-
+ {/if} -
- -
+ +
Listado de Tipos de Cambio
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/identifiers/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/identifiers/+page.svelte index 31be43a1..d986d669 100644 --- a/frontend/src/routes/dashboard/general_catalogs/identifiers/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/identifiers/+page.svelte @@ -4,17 +4,21 @@ import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/identifiers/columns'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/identifiers/create-edit-dialog.svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/identifiers/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaIdentificadores } from '$lib/config/shortcuts/dashboard/general_catalogs/identifiers/list'; + import { getIdentifiers } from '$lib/api/dashboard/a76/general_catalogs/identifiers'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,47 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.identifiers?.items || []); + let currentPage = $state(data.identifiers?.page || 1); + let pageSize = $state(data.identifiers?.page_size || 50); + let totalItems = $state(data.identifiers?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.identifiers) { + allItems = data.identifiers.items || []; + currentPage = data.identifiers.page || 1; + totalItems = data.identifiers.total || 0; + pageSize = data.identifiers.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getIdentifiers(1, pageSize, companyStore.activeCompany.id, filters); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,49 +82,125 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getIdentifiers( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more identifiers:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getIdentifiers(1, pageSize, companyStore.activeCompany.id, filters); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading identifiers:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Identificadores

Catálogo de identificadores

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Identificadores +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/inpc/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/inpc/+page.svelte index bc626c29..92990890 100644 --- a/frontend/src/routes/dashboard/general_catalogs/inpc/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/inpc/+page.svelte @@ -4,17 +4,21 @@ import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/inpc/columns'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edit-dialog.svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/inpc/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaInpc } from '$lib/config/shortcuts/dashboard/general_catalogs/inpc/list'; + import { getINPCs } from '$lib/api/dashboard/a76/general_catalogs/inpc'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,48 @@ let searchMonth = $state($page.url.searchParams.get('month') || ''); let timeout: ReturnType; - function handleSearch() { + // Infinite scroll + let allItems = $state(data.inpc?.items || []); + let currentPage = $state(data.inpc?.page || 1); + let pageSize = $state(data.inpc?.page_size || 50); + let totalItems = $state(data.inpc?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.inpc) { + allItems = data.inpc.items || []; + currentPage = data.inpc.page || 1; + totalItems = data.inpc.total || 0; + pageSize = data.inpc.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchYear) filters.year = searchYear; + if (searchMonth) filters.month = searchMonth; + + const response = await getINPCs(1, pageSize, companyStore.activeCompany.id, filters); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchYear) url.searchParams.set('year', searchYear); else url.searchParams.delete('year'); @@ -41,45 +83,125 @@ if (searchMonth) url.searchParams.set('month', searchMonth); else url.searchParams.delete('month'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchYear) filters.year = searchYear; + if (searchMonth) filters.month = searchMonth; + + const response = await getINPCs( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more INPC:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchYear) filters.year = searchYear; + if (searchMonth) filters.month = searchMonth; + + const response = await getINPCs(1, pageSize, companyStore.activeCompany.id, filters); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading INPC:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
-
+
+

INPC

Índice Nacional de Precios al Consumidor

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de INPC +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/legends/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/legends/+page.svelte index 2c9ecc30..9632ba7c 100644 --- a/frontend/src/routes/dashboard/general_catalogs/legends/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/legends/+page.svelte @@ -1,18 +1,22 @@ -
-
+
+

Leyendas Fijas

Gestión del catálogo de leyendas fijas

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Leyendas +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/multi_currency_types/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/multi_currency_types/+page.svelte index 1dbfa9d1..004b399a 100644 --- a/frontend/src/routes/dashboard/general_catalogs/multi_currency_types/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/multi_currency_types/+page.svelte @@ -1,17 +1,25 @@ -
+

Tipos de Moneda Múltiple

Gestión del catálogo de tipos de moneda múltiple

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Monedas
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/packages/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/packages/+page.svelte index 12777b93..659451d8 100644 --- a/frontend/src/routes/dashboard/general_catalogs/packages/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/packages/+page.svelte @@ -1,21 +1,25 @@ -
+

Bultos y Embalajes

Gestión del catálogo de bultos y embalajes

- -
- -
-
- -
-
- +
+ +
-
- + {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Bultos +
+ + +
+
+
+ +
+ +
+
+
+ +
+ Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte index 88c42d3c..2ad73e0c 100644 --- a/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/ports/+page.svelte @@ -4,17 +4,21 @@ import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/ports/columns'; import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaPuertos } from '$lib/config/shortcuts/dashboard/general_catalogs/ports/list'; + import { portsApi } from '$lib/api/dashboard/a76/general_catalogs/ports'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.items?.error || null); // Atajos useShortcuts( @@ -30,10 +34,50 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.items?.items || data.items || []); + let currentPage = $state(data.items?.page || 1); + let pageSize = $state(data.items?.pageSize || data.items?.page_size || 50); + let totalItems = $state(data.items?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.items?.items) { + allItems = data.items.items || []; + currentPage = data.items.page || 1; + totalItems = data.items.total || 0; + pageSize = data.items.pageSize || data.items.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const params: Record = { + page: '1', + page_size: pageSize.toString() + }; + if (searchCode) params.port_code = searchCode; + if (searchDesc) params.description = searchDesc; + + const response = await portsApi.list(companyStore.activeCompany.id, params); + if (response.data?.items) { + allItems = response.data.items; + currentPage = response.data.page || 1; + totalItems = response.data.total || 0; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('port_code', searchCode); else url.searchParams.delete('port_code'); @@ -41,58 +85,117 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const params: Record = { + page: (currentPage + 1).toString(), + page_size: pageSize.toString() + }; + if (searchCode) params.port_code = searchCode; + if (searchDesc) params.description = searchDesc; + + const response = await portsApi.list(companyStore.activeCompany.id, params); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total || 0; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more ports:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const params: Record = { + page: '1', + page_size: pageSize.toString() + }; + if (searchCode) params.port_code = searchCode; + if (searchDesc) params.description = searchDesc; + + const response = await portsApi.list(companyStore.activeCompany.id, params); + if (response.data?.items) { + allItems = response.data.items; + currentPage = response.data.page || 1; + totalItems = response.data.total || 0; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading ports:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Puertos

Catálogo de puertos

- +
+ + +
- {#if data.items?.error} + {#if error}
- {data.items.error} + {error}
{/if} -
-
- -
-
- -
-
+ + +
+ Listado de Puertos +
+ + +
+
+
+ +
+ +
+
+
-
- -
+
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/prevalidators/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/prevalidators/+page.svelte index 81899bfd..fd4ed81e 100644 --- a/frontend/src/routes/dashboard/general_catalogs/prevalidators/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/prevalidators/+page.svelte @@ -2,17 +2,21 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { browser } from '$app/environment'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/prevalidators/data-table.svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import { createColumns } from '$lib/components/dashboard/general_catalogs/prevalidators/columns'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaPrevalidadores } from '$lib/config/shortcuts/dashboard/general_catalogs/prevalidators/list'; + import { getPrevalidators } from '$lib/api/dashboard/a76/general_catalogs/prevalidators'; + import { companyStore } from '$lib/stores/company.svelte'; let { data } = $props(); let dialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -27,15 +31,47 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); - } + let allItems = $state(data.prevalidators?.items || []); + let currentPage = $state(data.prevalidators?.page || 1); + let pageSize = $state(data.prevalidators?.page_size || 50); + let totalItems = $state(data.prevalidators?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); - function handleSearch() { + $effect(() => { + if (data.prevalidators) { + allItems = data.prevalidators.items || []; + currentPage = data.prevalidators.page || 1; + totalItems = data.prevalidators.total || 0; + pageSize = data.prevalidators.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getPrevalidators(1, pageSize, filters, companyStore.activeCompany.id); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -43,45 +79,98 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getPrevalidators( + currentPage + 1, + pageSize, + filters, + companyStore.activeCompany.id + ); + if (response?.items) { + allItems = [...allItems, ...response.items]; + currentPage += 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more prevalidators:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getPrevalidators(1, pageSize, filters, companyStore.activeCompany.id); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading prevalidators:', err); + } finally { + loading = false; + } + } + + function handleSuccess() { + reloadData(); + } -
+

Prevalidadores

Catálogo de prevalidadores

- -
- -
-
- -
-
- +
+ +
-
- -
+ {#if error} +
+ {error} +
+ {/if} + + + +
+ Listado de Prevalidadores +
+ + +
+
+
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/seal/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/seal/+page.svelte index 562bb586..fac039f8 100644 --- a/frontend/src/routes/dashboard/general_catalogs/seal/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/seal/+page.svelte @@ -1,8 +1,9 @@ -
-
+
+

Sellos

Gestiona los sellos de tu empresa

- +
+ + +
{#if error} -
+

{error}

{/if} -
-
- -
-
+ + +
+ Listado de Sellos +
+ +
+
+
+ +
+ +
+
+
-
- +
+ Mostrando {allItems.length} de {totalItems || allItems.length} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/signatures/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/signatures/+page.svelte index 34af6e59..a0e46d78 100644 --- a/frontend/src/routes/dashboard/general_catalogs/signatures/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/signatures/+page.svelte @@ -4,15 +4,19 @@ import { browser } from '$app/environment'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/signatures/data-table.svelte'; + import * as Card from '$lib/components/ui/card'; + import { Plus, RefreshCw } from 'lucide-svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import { createColumns } from '$lib/components/dashboard/general_catalogs/signatures/columns'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaFirmas } from '$lib/config/shortcuts/dashboard/general_catalogs/signatures/list'; + import { getSignatures } from '$lib/api/dashboard/a76/general_catalogs/signatures'; + import { companyStore } from '$lib/stores/company.svelte'; let { data } = $props(); let dialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -26,50 +30,138 @@ let searchCode = $state($page.url.searchParams.get('code') || ''); let timeout: ReturnType; - function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); - } + let allItems = $state(data.signatures?.items || []); + let currentPage = $state(data.signatures?.page || 1); + let pageSize = $state(data.signatures?.page_size || 50); + let totalItems = $state(data.signatures?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); - function handleSearch() { + $effect(() => { + if (data.signatures) { + allItems = data.signatures.items || []; + currentPage = data.signatures.page || 1; + totalItems = data.signatures.total || 0; + pageSize = data.signatures.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + + const response = await getSignatures( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + + const response = await getSignatures( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response?.items) { + allItems = [...allItems, ...response.items]; + currentPage += 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more signatures:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + + const response = await getSignatures( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading signatures:', err); + } finally { + loading = false; + } + } + + function handleSuccess() { + reloadData(); + } -
+

Firmas Electrónicas

Gestión del catálogo de firmas electrónicas

- +
-
-
- + {#if error} +
+ {error}
-
+ {/if} -
- -
+ +
Listado de Firmas
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte index 9ad3119a..8fc9313b 100644 --- a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/+page.svelte @@ -89,145 +89,100 @@ } -
- - - - Catálogo de Fracciones SITAR - SCAII - -

Nomenclatura arancelaria mexicana completa

+
+
+
+

Catálogo de Fracciones SITAR - SCAII

+

Nomenclatura arancelaria mexicana completa

+
+ +
+ + + +
+ Listado de Fracciones +
+ + +
+
- - -
- -
-
- - -
- -
- - -
-
- {#if isLoading} -
- - Cargando... -
- {:else} - Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias - {#if searchQuery} - (filtrado) - {/if} - {/if} -
- {#if totalPages > 1} -
- Página {currentPage} de {totalPages} -
- {/if} -
- - -
- - + +
+ + + + Código + Fracción + Descripción + NICO + UMT + Adv. Impo + Adv. Expo + + + + {#if tariffFractions.length === 0} - Código - Fracción - Descripción - NICO - UMT - Adv. Impo - Adv. Expo + + {#if isLoading} + Cargando fracciones arancelarias... + {:else if searchQuery} + No se encontraron fracciones que coincidan con la búsqueda + {:else} + No hay fracciones arancelarias disponibles + {/if} + - - - {#if tariffFractions.length === 0} - - - {#if isLoading} - Cargando fracciones arancelarias... - {:else if searchQuery} - No se encontraron fracciones que coincidan con la búsqueda - {:else} - No hay fracciones arancelarias disponibles - {/if} - + {:else} + {#each tariffFractions as fraction (fraction.id)} + + {fraction.code} + {fraction.fraction} + {fraction.description || '-'} + {fraction.nico || '-'} + {fraction.umt || '-'} + {fraction.adv_impo || '-'} + {fraction.adv_expo || '-'} - {:else} - {#each tariffFractions as fraction (fraction.id)} - - {fraction.code} - {fraction.fraction} - - {fraction.description || '-'} - - {fraction.nico || '-'} - {fraction.umt || '-'} - {fraction.adv_impo || '-'} - {fraction.adv_expo || '-'} - - {/each} - {/if} - - -
- - - {#if totalPages > 1} -
- - - - Página {currentPage} de {totalPages} - - - -
- {/if} + {/each} + {/if} + +
+ +
+ Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias + {#if searchQuery} + + Filtrado + {/if} +
+ + {#if totalPages > 1} +
+ + + + +
+ {/if}
diff --git a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/canadian/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/canadian/+page.svelte index 5a67d55e..63fab84b 100644 --- a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/canadian/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/canadian/+page.svelte @@ -3,9 +3,15 @@ import * as m from '$lib/paraglide/messages.js'; -
-
-

{m['sidebar.fractions.canadian']()}

+
+
+
+

{m['sidebar.fractions.canadian']()}

+

Catálogo de fracciones arancelarias canadienses

+
+
+ +
+
-
diff --git a/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte index 596c3d13..0331dc38 100644 --- a/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte @@ -1,12 +1,13 @@ -
+
-

Trailers

+

Trailers

Gestión del catálogo de trailers de la compañía

- +
-
- - -
+ +
Listado de Trailers
+ {#if loading && data.length === 0}
Cargando trailers...
{:else}
{/if}
+
- {#if loading && data.length === 0} -
- Cargando trailers... -
- {:else} - - {/if} +
Mostrando {data.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte index 150e006b..f14bf171 100644 --- a/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte @@ -1,12 +1,13 @@ -
+
-

Transportistas

+

Transportistas

Gestión del catálogo de líneas transportistas

- +
-
- - -
+ +
Listado de Transportistas
+ {#if loading && data.length === 0}
Cargando transportistas...
{:else}
{/if}
+
- {#if loading && data.length === 0} -
- Cargando transportistas... -
- {:else} - - {/if} +
Mostrando {data.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/unit_conversions/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/unit_conversions/+page.svelte index bc274ff2..ea00c786 100644 --- a/frontend/src/routes/dashboard/general_catalogs/unit_conversions/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/unit_conversions/+page.svelte @@ -4,17 +4,21 @@ import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/unit_conversions/columns'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/unit_conversions/create-edit-dialog.svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/unit_conversions/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaConversiones } from '$lib/config/shortcuts/dashboard/general_catalogs/unit_conversions/list'; + import { getUnitConversions } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,52 @@ let searchTo = $state($page.url.searchParams.get('to_unit_code') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.conversions?.items || []); + let currentPage = $state(data.conversions?.page || 1); + let pageSize = $state(data.conversions?.page_size || 50); + let totalItems = $state(data.conversions?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.conversions) { + allItems = data.conversions.items || []; + currentPage = data.conversions.page || 1; + totalItems = data.conversions.total || 0; + pageSize = data.conversions.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchFrom) filters.from_unit_code = searchFrom; + if (searchTo) filters.to_unit_code = searchTo; + + const response = await getUnitConversions( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchFrom) url.searchParams.set('from_unit_code', searchFrom); else url.searchParams.delete('from_unit_code'); @@ -41,46 +87,92 @@ if (searchTo) url.searchParams.set('to_unit_code', searchTo); else url.searchParams.delete('to_unit_code'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchFrom) filters.from_unit_code = searchFrom; + if (searchTo) filters.to_unit_code = searchTo; + + const response = await getUnitConversions( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response?.items) { + allItems = [...allItems, ...response.items]; + currentPage += 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more unit conversions:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchFrom) filters.from_unit_code = searchFrom; + if (searchTo) filters.to_unit_code = searchTo; + + const response = await getUnitConversions( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response?.items) { + allItems = response.items; + currentPage = 1; + totalItems = response.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading unit conversions:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Conversiones de Unidades

Catálogo de conversiones de unidades de medida

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Conversiones
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/ace/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/ace/+page.svelte index 664840ef..a4bc68fb 100644 --- a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/ace/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/ace/+page.svelte @@ -4,17 +4,21 @@ import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/columns'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/create-edit-dialog.svelte'; - import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaUnidadesACE } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/ace/list'; + import { getUnitsOfMeasureACE } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,52 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.ace_units?.items || []); + let currentPage = $state(data.ace_units?.page || 1); + let pageSize = $state(data.ace_units?.page_size || 50); + let totalItems = $state(data.ace_units?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.ace_units) { + allItems = data.ace_units.items || []; + currentPage = data.ace_units.page || 1; + totalItems = data.ace_units.total || 0; + pageSize = data.ace_units.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureACE( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,50 +87,92 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureACE( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more ACE units:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureACE( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading ACE units:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Unidades de Medida ACE

Catálogo de unidades de medida ACE

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Unidades ACE
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/american/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/american/+page.svelte index 41f27a3f..38bfedf3 100644 --- a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/american/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/american/+page.svelte @@ -3,18 +3,22 @@ import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/american/columns'; - import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/american/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/american/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaUnidadesAmericanas } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/american/list'; + import { getUnitsOfMeasureAmerican } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,52 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.american_units?.items || []); + let currentPage = $state(data.american_units?.page || 1); + let pageSize = $state(data.american_units?.page_size || 50); + let totalItems = $state(data.american_units?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.american_units) { + allItems = data.american_units.items || []; + currentPage = data.american_units.page || 1; + totalItems = data.american_units.total || 0; + pageSize = data.american_units.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureAmerican( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,50 +87,92 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureAmerican( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more American units:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureAmerican( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading American units:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Unidades de Medida Americanas

Catálogo de unidades de medida Americanas

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Unidades Americanas
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/customs/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/customs/+page.svelte index 671625b1..e8e8fe79 100644 --- a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/customs/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/customs/+page.svelte @@ -3,18 +3,22 @@ import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/columns'; - import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaUnidadesAduanas } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/customs/list'; + import { getUnitsOfMeasureCustoms } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,52 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.customs_units?.items || []); + let currentPage = $state(data.customs_units?.page || 1); + let pageSize = $state(data.customs_units?.page_size || 50); + let totalItems = $state(data.customs_units?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.customs_units) { + allItems = data.customs_units.items || []; + currentPage = data.customs_units.page || 1; + totalItems = data.customs_units.total || 0; + pageSize = data.customs_units.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureCustoms( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,50 +87,92 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureCustoms( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more customs units:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureCustoms( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading customs units:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Unidades de Medida Aduanas MEX

Catálogo de unidades de medida para aduanas mexicanas

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Unidades Aduanas MX
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/general/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/general/+page.svelte index 92a5cdd1..00f06498 100644 --- a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/general/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/general/+page.svelte @@ -3,18 +3,22 @@ import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/general/columns'; - import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/general/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/general/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaUnidadesGeneral } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/general/list'; + import { getUnitsOfMeasureGeneral } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,52 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.general_units?.items || []); + let currentPage = $state(data.general_units?.page || 1); + let pageSize = $state(data.general_units?.page_size || 50); + let totalItems = $state(data.general_units?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.general_units) { + allItems = data.general_units.items || []; + currentPage = data.general_units.page || 1; + totalItems = data.general_units.total || 0; + pageSize = data.general_units.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureGeneral( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,50 +87,92 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureGeneral( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more general units:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureGeneral( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading general units:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Unidades de Medida

Catálogo general de unidades de medida

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Unidades
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/oma/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/oma/+page.svelte index eab0d9a6..da28b5ba 100644 --- a/frontend/src/routes/dashboard/general_catalogs/units_of_measure/oma/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/units_of_measure/oma/+page.svelte @@ -3,18 +3,22 @@ import { goto } from '$app/navigation'; import { browser } from '$app/environment'; import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/columns'; - import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/data-table.svelte'; + import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte'; import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { Plus, RefreshCw } from 'lucide-svelte'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaUnidadesOMA } from '$lib/config/shortcuts/dashboard/general_catalogs/units_of_measure/oma/list'; + import { getUnitsOfMeasureOMA } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure'; + import { companyStore } from '$lib/stores/company.svelte'; let { data }: { data: PageData } = $props(); let createDialogOpen = $state(false); + let error = $state(data.error || null); // Atajos useShortcuts( @@ -30,10 +34,52 @@ let searchDesc = $state($page.url.searchParams.get('description') || ''); let timeout: ReturnType; - function handleSearch() { + let allItems = $state(data.oma_units?.items || []); + let currentPage = $state(data.oma_units?.page || 1); + let pageSize = $state(data.oma_units?.page_size || 50); + let totalItems = $state(data.oma_units?.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + + $effect(() => { + if (data.oma_units) { + allItems = data.oma_units.items || []; + currentPage = data.oma_units.page || 1; + totalItems = data.oma_units.total || 0; + pageSize = data.oma_units.page_size || pageSize; + } + }); + + async function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureOMA( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error aplicando filtros'; + console.error('Error applying filters:', err); + } finally { + loading = false; + } + const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,50 +87,92 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } + async function loadMore() { + if (loading || !hasMore || !companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureOMA( + currentPage + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage += 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error cargando mas datos'; + console.error('Error loading more OMA units:', err); + } finally { + loading = false; + } + } + + async function reloadData() { + if (!companyStore.activeCompany) return; + loading = true; + error = null; + try { + const filters: Record = {}; + if (searchCode) filters.code = searchCode; + if (searchDesc) filters.description = searchDesc; + + const response = await getUnitsOfMeasureOMA( + 1, + pageSize, + companyStore.activeCompany.id, + filters + ); + if (response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (err) { + error = 'Error al recargar datos'; + console.error('Error reloading OMA units:', err); + } finally { + loading = false; + } + } + function handleSuccess() { - const url = new URL($page.url); - goto(url, { invalidateAll: true }); + reloadData(); } -
+

Unidades de Medida OMA

Catálogo de unidades de medida OMA

- +
-
-
- + {#if error} +
+ {error}
-
- -
-
+ {/if} -
- -
+ +
Listado de Unidades OMA
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte index 1bcc8916..39ee3719 100644 --- a/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte @@ -1,12 +1,13 @@ -
+
-

Vehículos (Transporte)

+

Vehículos (Transporte)

Gestión del catálogo de camiones y vehículos de transporte

- +
-
- - -
+ +
Listado de Vehículos
+ {#if loading && data.length === 0}
Cargando vehículos...
{:else}
{/if}
+
- {#if loading && data.length === 0} -
- Cargando vehículos... -
- {:else} - - {/if} +
Mostrando {data.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte index 3b9bf873..7f46bab4 100644 --- a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte +++ b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte @@ -233,19 +233,30 @@ ]); -
- -
-

CATALOGO DE CLASES DE ACTIVO FIJO

-

Gestiona y consulta las clases de activo fijo

+
+
+
+

Clases de Activo Fijo

+

Gestiona y consulta las clases de activo fijo

+
+
+ + +
-
+
-
+
-
+

Filtros

@@ -256,37 +267,37 @@
- +
- +
- +
-
-
+
+

Listado de Clases

Mostrando de {filteredClasses.length} registros - @@ -309,9 +320,7 @@
-
+

Código de Clase @@ -382,6 +391,8 @@

+
+
-
- -
-

CATÁLOGO DE PARTES

-

- Gestiona y consulta las partes de inventario y activo fijo -

+
+
+
+

Catálogo de Partes

+

+ Gestiona y consulta las partes de inventario y activo fijo +

+
+
+ + +
-
+
-
+
-
+

Filtros

@@ -215,23 +226,23 @@
- +
- +
- +
@@ -267,14 +278,14 @@
-
-
+
+

Listado de Partes

Mostrando {filteredParts.length} registros - @@ -298,9 +309,7 @@
-
+

Número de Parte @@ -414,6 +423,8 @@

+
+
([]); @@ -81,15 +94,15 @@ const urlOperationType = searchParams.get('operation_type'); const urlInvoiceType = searchParams.get('invoice_type'); const urlInvoiceNumber = searchParams.get('invoice_number'); - const urlProjectNumber = searchParams.get('project_number'); - const urlYear = searchParams.get('year'); + const urlYearFrom = searchParams.get('year_from'); + const urlYearTo = searchParams.get('year_to'); // Actualizar filtros si hay cambios en la URL filters.operation_type = (urlOperationType || '') as '' | OperationType; filters.invoice_type = urlInvoiceType || ''; filters.invoice_number = urlInvoiceNumber || ''; - filters.project_number = urlProjectNumber || ''; - filters.year = urlYear || ''; + filters.year_from = urlYearFrom || ''; + filters.year_to = urlYearTo || ''; } }); @@ -118,8 +131,8 @@ if (filters.operation_type) params.set('operation_type', filters.operation_type); if (filters.invoice_type) params.set('invoice_type', filters.invoice_type); if (filters.invoice_number) params.set('invoice_number', filters.invoice_number); - if (filters.project_number) params.set('project_number', filters.project_number); - if (filters.year) params.set('year', filters.year); + if (filters.year_from) params.set('year_from', filters.year_from); + if (filters.year_to) params.set('year_to', filters.year_to); const queryString = params.toString(); const newUrl = queryString ? `?${queryString}` : window.location.pathname; @@ -140,8 +153,8 @@ operation_type: filters.operation_type, invoice_type: filters.invoice_type, invoice_number: filters.invoice_number, - project_number: filters.project_number, - year: filters.year + year_from: filters.year_from, + year_to: filters.year_to }; const currentFiltersKey = JSON.stringify(currentFilters); @@ -217,6 +230,17 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Submenú contextual para Interface VU (click derecho) + let showVuSubmenu = $state(false); + let vuSubmenuPosition = $state({ x: 0, y: 0 }); + + // Si se pierde la selección, cerramos el submenú contextual VU + $effect(() => { + if (!hasSelection) { + showVuSubmenu = false; + } + }); + // Estado para selección de filas (múltiple) let selectedInvoiceIds = $state([]); // Estado para los diálogos de acciones @@ -257,8 +281,8 @@ operation_type: filters.operation_type || undefined, invoice_type: filters.invoice_type || undefined, invoice_number: filters.invoice_number || undefined, - project_number: filters.project_number || undefined, - year: filters.year || undefined, + year_from: filters.year_from || undefined, + year_to: filters.year_to || undefined, sort_by: sorting.length > 0 ? sorting[0].id : undefined, sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined }; @@ -316,8 +340,8 @@ operation_type: filters.operation_type || undefined, invoice_type: filters.invoice_type || undefined, invoice_number: filters.invoice_number || undefined, - project_number: filters.project_number || undefined, - year: filters.year || undefined, + year_from: filters.year_from || undefined, + year_to: filters.year_to || undefined, sort_by: sorting.length > 0 ? sorting[0].id : undefined, sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined }; @@ -356,8 +380,8 @@ operation_type: '', invoice_type: '', invoice_number: '', - project_number: '', - year: '' + year_from: '', + year_to: '' }; applyFilters(); } @@ -376,8 +400,8 @@ operation_type: filters.operation_type || undefined, invoice_type: filters.invoice_type || undefined, invoice_number: filters.invoice_number || undefined, - project_number: filters.project_number || undefined, - year: filters.year || undefined, + year_from: filters.year_from || undefined, + year_to: filters.year_to || undefined, sort_by: sorting.length > 0 ? sorting[0].id : undefined, sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined }; @@ -414,9 +438,189 @@ // Estado para el diálogo de progreso let showProgressDialog = $state(false); let isWinsaiiConfirmOpen = $state(false); + let isCoveDialogOpen = $state(false); let currentTaskId = $state(null); let currentStatusFunction = $state<((taskId: string) => Promise) | null>(null); let progressDialogTitle = $state('Generando documento'); + let progressDialogSteps = $state<{ label: string; percent: number }[] | null>(null); + let coveRecipientsLoading = $state(false); + let coveRecipientsError = $state(null); + let coveRecipientEmail = $state(''); + let coveRecipientOptions = $state([]); + let coveRecipientsRequestId = 0; + + const currentUserEmail = $derived(($currentUser?.email || '').trim()); + const selectedCoveRecipient = $derived( + coveRecipientOptions.find( + (option) => option.email.trim().toLowerCase() === coveRecipientEmail.trim().toLowerCase() + ) ?? null + ); + + function normalizeEmail(email?: string | null) { + return (email || '').trim().toLowerCase(); + } + + function createRecipientLabel(prefix: string, email: string) { + return `${prefix} - ${email}`; + } + + async function loadCoveRecipients() { + const companyId = companyStore.activeCompany?.id; + if (!companyId) { + coveRecipientsError = null; + coveRecipientOptions = []; + coveRecipientEmail = ''; + return; + } + + const requestId = ++coveRecipientsRequestId; + coveRecipientsLoading = true; + coveRecipientsError = null; + + try { + const [companyResult, usersResult] = await Promise.allSettled([ + getCompany(companyId), + usersAPI.list(companyId, { page_size: 100 }) + ]); + + if (requestId !== coveRecipientsRequestId) return; + + const recipientMap = new Map(); + + const addRecipient = ( + email?: string | null, + label?: string, + description?: string, + source: 'company' | 'user' = 'user' + ) => { + const normalizedEmail = normalizeEmail(email); + if (!normalizedEmail) return; + if (recipientMap.has(normalizedEmail)) return; + recipientMap.set(normalizedEmail, { + email: email!.trim(), + label: label || email!.trim(), + description: description || email!.trim(), + source + }); + }; + + if (companyResult.status === 'fulfilled' && companyResult.value.data) { + const company = companyResult.value.data as Record; + addRecipient( + company.vu_email, + 'Correo VU de la empresa', + company.name ? `Empresa ${company.name}` : 'Correo de ventanilla única', + 'company' + ); + addRecipient( + company.main_email, + 'Correo principal de la empresa', + company.name ? `Empresa ${company.name}` : 'Correo principal', + 'company' + ); + addRecipient( + company.ind1_email, + 'Correo industrial 1', + company.name ? `Empresa ${company.name}` : 'Correo industrial 1', + 'company' + ); + addRecipient( + company.ind2_email, + 'Correo industrial 2', + company.name ? `Empresa ${company.name}` : 'Correo industrial 2', + 'company' + ); + } + + if (usersResult.status === 'fulfilled' && usersResult.value.users) { + for (const user of usersResult.value.users) { + const fullName = `${user.first_name || ''} ${user.last_name || ''}`.trim(); + addRecipient( + user.email, + fullName || user.username || user.email, + createRecipientLabel('Usuario de la empresa', user.email), + 'user' + ); + } + } + + if (currentUserEmail) { + addRecipient( + currentUserEmail, + 'Mi correo', + createRecipientLabel('Usuario autenticado', currentUserEmail), + 'user' + ); + } + + const recipients = [...recipientMap.values()].sort((left, right) => { + if (left.source !== right.source) { + return left.source === 'company' ? -1 : 1; + } + return left.label.localeCompare(right.label, 'es'); + }); + + const sourceFailures = [companyResult, usersResult].filter( + (result) => result.status === 'rejected' + ).length; + + coveRecipientOptions = recipients; + + const preferredEmail = normalizeEmail(currentUserEmail); + const existingSelection = + recipients.find((option) => normalizeEmail(option.email) === normalizeEmail(coveRecipientEmail)) || + null; + const preferredSelection = + recipients.find((option) => normalizeEmail(option.email) === preferredEmail) || recipients[0] || null; + + if (existingSelection) { + coveRecipientEmail = existingSelection.email; + } else if (preferredSelection) { + coveRecipientEmail = preferredSelection.email; + } else { + coveRecipientEmail = ''; + } + + if (recipientMap.size === 0 && sourceFailures > 0) { + coveRecipientsError = 'No se pudieron cargar los correos disponibles para COVE'; + } else if (recipientMap.size === 0) { + coveRecipientsError = 'No hay correos configurados para COVE'; + } + } catch (error) { + if (requestId !== coveRecipientsRequestId) return; + console.error('Error cargando correos de COVE:', error); + coveRecipientsError = 'No se pudieron cargar los correos disponibles para COVE'; + } finally { + if (requestId === coveRecipientsRequestId) { + coveRecipientsLoading = false; + } + } + } + + $effect(() => { + const companyId = companyStore.activeCompany?.id; + const userEmail = currentUserEmail; + + if (!companyId) { + coveRecipientsError = null; + coveRecipientOptions = []; + coveRecipientEmail = ''; + return; + } + + void userEmail; + void loadCoveRecipients(); + }); + + function openCoveDialog() { + if (!selectedInvoice || !companyStore.activeCompany) { + toast.info('Selecciona una factura para generar COVE'); + return; + } + + void loadCoveRecipients(); + isCoveDialogOpen = true; + } // Utilidad para convertir Base64 a Blob function base64ToBlob(base64: string, type: string) { @@ -614,11 +818,27 @@ window.URL.revokeObjectURL(url); document.body.removeChild(a); toast.success('PDF Descargado exitosamente'); + } else if (result.cove_number) { + // Resultado de generación de COVE + const baseMsg = `COVE generado correctamente: ${result.cove_number}`; + const opMsg = result.vucem_operation_num + ? ` (Operación VUCEM: ${result.vucem_operation_num})` + : ''; + toast.success(baseMsg + opMsg); + reloadData(); } else { - // Resultado de procesamiento de factura + // Resultado de procesamiento de factura (import process/revert) toast.success('Factura procesada correctamente'); reloadData(); } + } else if (result.status === 'external_queued') { + // Caso especial: VU aceptó la factura y la dejó en cola, pero devuelve + // un mensaje tipo "Factura COVE iniciada para: ... Use el task_id para consultar el estado." + const baseMsg = + result.message || + 'Factura COVE iniciada en Ventanilla Única. Use el task_id para consultar el estado.'; + const taskInfo = result.external_task_id ? ` (task_id: ${result.external_task_id})` : ''; + toast.success(baseMsg + taskInfo); } else if (result.status === 'validation_error') { const errors: any[] = result.errors || []; const preview = errors @@ -627,18 +847,21 @@ .join('\n'); const extra = errors.length > 3 ? `\n...y ${errors.length - 3} más` : ''; toast.error(`${errors.length} error(es) de validación:\n${preview}${extra}`); + } else if ( + typeof result.message === 'string' && + result.message.includes('Factura COVE iniciada para') + ) { + // Salvaguarda: si por alguna razón el status no vino como external_queued + // pero el mensaje es el de "Factura COVE iniciada...", lo tratamos también + // como éxito/en cola y no como error. + const taskInfo = result.external_task_id ? ` (task_id: ${result.external_task_id})` : ''; + toast.success(result.message + taskInfo); } else { toast.error('El worker reportó un error: ' + (result.message || 'Desconocido')); } } catch (e) { console.error('Error al procesar resultado:', e); toast.error('Error al procesar el resultado de la tarea'); - } finally { - // Cerrar diálogo después de un breve momento - setTimeout(() => { - showProgressDialog = false; - currentTaskId = null; - }, 1000); } } @@ -743,6 +966,7 @@ currentTaskId = response.data!.task_id; currentStatusFunction = invoicesApi.getProcessStatus; progressDialogTitle = 'Procesando factura'; + progressDialogSteps = invoiceProcessSteps; showProgressDialog = true; } catch (e) { console.error('Error al iniciar proceso de factura:', e); @@ -768,6 +992,7 @@ currentTaskId = response.data!.task_id; currentStatusFunction = invoicesApi.getRevertStatus; progressDialogTitle = 'Des-actualizando factura'; + progressDialogSteps = invoiceRevertSteps; showProgressDialog = true; } catch (e) { console.error('Error al iniciar des-actualización de factura:', e); @@ -775,6 +1000,70 @@ } } + async function handleGenerateCove(recipientEmail = coveRecipientEmail) { + if (!selectedInvoice || !companyStore.activeCompany) { + toast.info('Selecciona una factura para generar COVE'); + return; + } + + const selectedRecipientEmail = normalizeEmail(recipientEmail); + if (!selectedRecipientEmail) { + toast.error('Selecciona un correo para enviar el COVE'); + return; + } + + const companyId = companyStore.activeCompany.id; + + // Paso 1: Checar elegibilidad antes de disparar la tarea + try { + const elig = await invoicesApi.checkCoveEligibility(selectedInvoice.id, companyId); + if (elig.error) { + toast.error(`No se pudo validar elegibilidad COVE: ${elig.error}`); + return; + } + if (elig.data && !elig.data.can_generate) { + const msg = + elig.data.reasons + ?.map((r) => `• ${r.message}`) + .join('\n') || + 'La factura no cumple los requisitos para generar COVE'; + toast.error(msg); + return; + } + } catch (e) { + console.error('Error verificando elegibilidad COVE:', e); + toast.error('No se pudo verificar si la factura puede generar COVE'); + return; + } + + // Paso 2: Disparar tarea Celery de COVE + try { + const response = await invoicesApi.generateCove( + selectedInvoice.id, + companyId, + selectedRecipientEmail + ); + + if (response.error) { + toast.error(`Error al iniciar generación de COVE: ${response.error}`); + return; + } + + isCoveDialogOpen = false; + + currentTaskId = response.data!.task_id; + currentStatusFunction = invoicesApi.getCoveStatus; + progressDialogTitle = 'Validando datos para COVE'; + // Para COVE queremos UNA sola barra de progreso que refleje + // directamente el porcentaje reportado por VU, sin pasos fijos. + progressDialogSteps = null; + showProgressDialog = true; + } catch (e) { + console.error('Error al iniciar generación de COVE:', e); + toast.error('No se pudo iniciar la generación de COVE'); + } + } + // Pasos del procesamiento de factura (deben coincidir con el backend) const invoiceProcessSteps = [ { label: 'Cargando factura', percent: 5 }, @@ -793,6 +1082,14 @@ { label: 'Confirmando cambios', percent: 95 } ]; + const invoiceCoveSteps = [ + { label: 'Validando factura para COVE', percent: 10 }, + { label: 'Validando configuración VU', percent: 30 }, + { label: 'Validando emisor/destinatario', percent: 50 }, + { label: 'Validando mercancías para COVE', percent: 80 }, + { label: 'Finalizando validaciones de COVE', percent: 95 } + ]; + // Opciones de tipo de operación para el filtro const operationTypeOptions = [ { value: '', label: 'Todas' }, @@ -829,6 +1126,7 @@ showProgressDialog = false; currentTaskId = null; currentStatusFunction = null; + progressDialogSteps = null; } // --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS --- @@ -863,84 +1161,47 @@ } -
+
-

Facturas

+

Facturas

Gestiona las facturas del sistema

-
- -
- - - Filtros - Filtra las facturas por diferentes criterios (los filtros se aplican automáticamente) - - -
-
- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
-
{#if error} @@ -951,38 +1212,158 @@ {/if} - +
Listado de Facturas - - Mostrando {allItems.length} de {totalItems} registros -
-
-
- - (sorting = newSorting)} - /> + +
+ (sorting = newSorting)} + /> +
+
+ Mostrando {allItems.length} de {totalItems} registros + + Filtros activos: {Object.values(filters).filter((value) => value !== '').length} +
+ +
+ + + + + Generar COVE + + Selecciona el correo destinatario para la factura + {selectedInvoice?.invoice_number}. + + + + + +
+ + + + {#snippet children()} +
+
+ +
+
+
+ Destino COVE +
+
+ {selectedCoveRecipient?.label || 'Selecciona un correo'} +
+
+ {selectedCoveRecipient?.email || 'Se enviará al correo del usuario que generó la factura'} +
+
+
+ {/snippet} +
+ + {#if coveRecipientsLoading} +
+ Cargando correos disponibles... +
+ {:else if coveRecipientOptions.length === 0} +
+ No hay correos disponibles para COVE. +
+ {:else} + {#each coveRecipientOptions as recipient} + + {#snippet children({ selected })} +
+ + {recipient.label} + {#if selected} + Seleccionado + {/if} + + + {recipient.description} + +
+ {/snippet} +
+ {/each} + {/if} +
+
+ {#if coveRecipientsError} +

{coveRecipientsError}

+ {/if} +
+
+
+ + + + + +
+
+ @@ -1042,6 +1427,68 @@ + {#if showVuSubmenu} + +
(showVuSubmenu = false)} + oncontextmenu={(event) => { + event.preventDefault(); + showVuSubmenu = false; + }} + >
+ +
+ + + + +
+ {/if} +
- + { + if (!open) { + showVuSubmenu = false; + } + }} + > {#snippet child({ props })} +
diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index f04b8f3d..5da5e0ca 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -329,8 +329,8 @@ electronic_signature: invoice.compliance_mx?.electronic_signature || '', mandatory_person: invoice.compliance_mx?.mandatory_person || '', contingency_mode: invoice.compliance_mx?.contingency_mode || false, - cove: invoice.compliance_mx?.cove || '', - operation_num: invoice.compliance_mx?.operation_num || '', + cove: invoice.compliance_mx?.edocument || '', + operation_num: invoice.compliance_mx?.vucem_operation_num || '', adendas: invoice.compliance_mx?.adendas || '', observations_vu: invoice.compliance_mx?.observations_vu || '', certified_number: invoice.compliance_mx?.certified_number || '', diff --git a/frontend/src/routes/dashboard/pedimentos/+page.svelte b/frontend/src/routes/dashboard/pedimentos/+page.svelte index 34f59e23..0fedc396 100644 --- a/frontend/src/routes/dashboard/pedimentos/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/+page.svelte @@ -11,7 +11,7 @@ import type { PageData } from './$types'; import { browser } from '$app/environment'; import { companyStore } from '$lib/stores/company.svelte'; - import { Edit, Send, Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte'; +import { Edit, Send, Plus, Trash2, RefreshCw } from 'lucide-svelte'; import { obtenerAtajosListaPedimento } from '$lib/config/shortcuts/dashboard/pedimentos/list'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { goto } from '$app/navigation'; @@ -24,12 +24,12 @@ // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para filtros - let filters = $state({ - status: '', - client_id: '', - year: '' - }); +// Estado para filtros +let filters = $state({ + status: '', + pedimento: '', + year: '' +}); let sorting = $state([{ id: 'id', desc: true }]); @@ -256,7 +256,7 @@ const filterParams = { status: filters.status || undefined, - client_id: filters.client_id ? parseInt(filters.client_id) : undefined, + pedimento: filters.pedimento || undefined, year: filters.year || undefined, sort_by: sorting.length > 0 ? sorting[0].id : undefined, sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined @@ -309,7 +309,7 @@ const filterParams = { status: filters.status || undefined, - client_id: filters.client_id ? parseInt(filters.client_id) : undefined, + pedimento: filters.pedimento || undefined, year: filters.year || undefined, sort_by: sorting.length > 0 ? sorting[0].id : undefined, sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined @@ -347,7 +347,7 @@ function clearFilters() { filters = { status: '', - client_id: '', + pedimento: '', year: '' }; applyFilters(); @@ -370,7 +370,7 @@ const filterParams = { status: filters.status || undefined, - client_id: filters.client_id ? parseInt(filters.client_id) : undefined, + pedimento: filters.pedimento || undefined, year: filters.year || undefined, sort_by: sorting.length > 0 ? sorting[0].id : undefined, sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined @@ -439,69 +439,37 @@ const columns = createColumns(handleSuccess); -
+
-

Pedimentos

+

Pedimentos

Gestiona los pedimentos del sistema

- +
+ + +
- - - -
-
- - -
- -
- - -
- -
- - -
-
-
-
- {#if error} - + Error {error} @@ -510,42 +478,64 @@ {/if} - +
Listado de Pedimentos - - Mostrando {allItems.length} de {totalItems} registros -
- +
+ + + +
- + - (sorting = newSorting)} - /> +
+ (sorting = newSorting)} + /> +
+
+ Mostrando {allItems.length} de {totalItems} registros + + Filtros activos: {Object.values(filters).filter((value) => value !== '').length} +
+ +
+
diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte index 5f0781a8..0ecf1a06 100644 --- a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte @@ -3,56 +3,39 @@ import { codePedimentoRegimensApi, type CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens'; import DataTable from '$lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/code_pedimento_regimens/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; + import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/code_pedimento_regimens/list'; - import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos useShortcuts( 'Lista Códigos', obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), manejarActualizar: reloadData }) ); - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -65,116 +48,101 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await codePedimentoRegimensApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await codePedimentoRegimensApi.list(currentPage + 1, pageSize); - + try { + const response = await codePedimentoRegimensApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess const columns = createColumns(handleSuccess); -
- -
-
-

Código Pedimento - Regímenes

+
+ +
+
+

+ Pedimento - Regímenes +

- Gestiona las relaciones entre códigos de pedimento y regímenes + Relación entre códigos de pedimento y regímenes aduaneros

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Relaciones - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Pedimento - Regímenes
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/containers/+page.svelte b/frontend/src/routes/dashboard/reference_data/containers/+page.svelte index fdb94c31..f25afd54 100644 --- a/frontend/src/routes/dashboard/reference_data/containers/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/containers/+page.svelte @@ -3,56 +3,30 @@ import { containersApi, type Container } from '$lib/api/dashboard/reference_data/containers'; import DataTable from '$lib/components/dashboard/reference_data/containers/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/containers/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/containers/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/containers/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Contenedores', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -65,116 +39,112 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await containersApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await containersApi.list(currentPage + 1, pageSize); - + try { + const response = await containersApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Contenedores', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
- -
-
-

Contenedores

+
+ +
+
+

+ Contenedores +

- Gestiona los tipos de contenedores disponibles + Gestiona los tipos de contenedores disponibles en el sistema

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - + -
-
- Listado de Contenedores - - Mostrando {allItems.length} de {totalItems} registros - +
+ Listado de Contenedores +
+
-
- - - - +
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/countries/+page.svelte b/frontend/src/routes/dashboard/reference_data/countries/+page.svelte index 6a66336f..378f0264 100644 --- a/frontend/src/routes/dashboard/reference_data/countries/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/countries/+page.svelte @@ -3,9 +3,11 @@ import { countriesApi, type Country } from '$lib/api/dashboard/reference_data/countries'; import DataTable from '$lib/components/dashboard/reference_data/countries/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/countries/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { Input } from '$lib/components/ui/input'; + import { RefreshCw } from 'lucide-svelte'; + import { page } from '$app/stores'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/countries/list'; @@ -14,14 +16,10 @@ // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos useShortcuts( 'Lista Países', obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), manejarActualizar: reloadData }) ); @@ -64,6 +62,37 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await countriesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + + // Actualizar URL silenciosamente para mantener estado + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; @@ -71,7 +100,7 @@ error = null; try { - const response = await countriesApi.list(currentPage + 1, pageSize); + const response = await countriesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); @@ -108,10 +137,6 @@ window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { // Recargar datos después de crear/editar/eliminar reloadData(); @@ -121,41 +146,35 @@ const columns = createColumns(handleSuccess); -
-
-
-

Países

+
+ +
+
+

+ Países +

Gestiona los países disponibles en el sistema

-
- -
{#if error} -
+
{error}
{/if} -
- -
-
+ +
Listado de Países
+
+
- +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte index b546fb2a..50b1085f 100644 --- a/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte @@ -3,55 +3,30 @@ import { currencyTypesApi, type CurrencyType } from '$lib/api/dashboard/reference_data/currency_types'; import DataTable from '$lib/components/dashboard/reference_data/currency_types/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/currency_types/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/currency_types/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Tipos de Moneda', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -64,98 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await currencyTypesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await currencyTypesApi.list(currentPage + 1, pageSize); - + try { + const response = await currencyTypesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Tipos de Moneda', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
-
-
-

Tipos de Moneda

+
+ +
+
+

+ Tipos de Moneda +

Gestiona los tipos de moneda disponibles en el sistema

-
- -
{#if error} -
+
{error}
{/if} -
- -
-
+ +
Listado de Tipos de Moneda
+
+
- +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte index 4c59e6a0..2a6da5e7 100644 --- a/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte @@ -3,46 +3,30 @@ import { customsSectionsApi, type CustomsSection } from '$lib/api/dashboard/reference_data/customs_sections'; import DataTable from '$lib/components/dashboard/reference_data/customs_sections/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/customs_sections/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/customs_sections/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -55,122 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await customsSectionsApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - try { - const response = await customsSectionsApi.list(currentPage + 1, pageSize); - + const response = await customsSectionsApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - if (response.data?.items) { - // Agregar los nuevos items al array existente allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Keyboard Shortcuts useShortcuts('Secciones Aduanales', [ - { - key: 'Alt+Shift+N', - description: 'Nueva Sección', - action: handleCreateClick - }, - { - key: 'Alt+Shift+R', - description: 'Actualizar Lista', - action: reloadData - } + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } ]); - // Crear columnas con el callback onSuccess const columns = createColumns(handleSuccess); -
- -
-
-

Secciones Aduanales

-

Gestiona las secciones aduanales del sistema

+
+ +
+
+

+ Secciones Aduanales +

+

+ Gestiona las secciones aduanales del sistema +

+
+
+
-
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Secciones Aduanales - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Secciones Aduanales
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte index 7055ef57..649546c2 100644 --- a/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte @@ -3,55 +3,30 @@ import { customsWarehousesApi, type CustomsWarehouse } from '$lib/api/dashboard/reference_data/customs_warehouses'; import DataTable from '$lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/customs_warehouses/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/customs_warehouses/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/customs_warehouses/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Almacenes Aduanales', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -64,98 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await customsWarehousesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await customsWarehousesApi.list(currentPage + 1, pageSize); - + try { + const response = await customsWarehousesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Almacenes Aduanales', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
-
-
-

Recintos Fiscalizados

+
+ +
+
+

+ Recintos Fiscalizados +

Gestiona los recintos fiscalizados del sistema aduanal

-
- -
{#if error} -
+
{error}
{/if} -
- -
-
+ +
Listado de Recintos Fiscalizados
+
+
- +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts b/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts index 776fe196..0527ad0a 100644 --- a/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts @@ -18,13 +18,20 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { } try { - // Obtener parámetros de paginación de la URL + // Obtener parámetros de paginación y filtros de la URL const page = parseInt(url.searchParams.get('page') || '1'); const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + const code = url.searchParams.get('code') || ''; + const description = url.searchParams.get('description') || ''; + + // Construir URL con filtros + let endpoint = `v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`; + if (code) endpoint += `&code=${encodeURIComponent(code)}`; + if (description) endpoint += `&description=${encodeURIComponent(description)}`; // Usar authenticatedFetch para manejar automáticamente el refresh de tokens const response = await authenticatedFetch( - `v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`, + endpoint, {}, cookies, fetch diff --git a/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte b/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte index 9a1b417c..5288a8a8 100644 --- a/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte @@ -2,29 +2,76 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { browser } from '$app/environment'; + import { incotermsApi, type Incoterm } from '$lib/api/dashboard/reference_data/incoterms'; import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns'; import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/incoterms/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Plus } from 'lucide-svelte'; + import { RefreshCw, Plus } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/incoterms/list'; import type { PageData } from './$types'; let { data }: { data: PageData } = $props(); - let createDialogOpen = $state(false); - // Atajos useShortcuts( 'Lista Incoterms', obtenerAtajosLista({ - manejarNuevo: () => (createDialogOpen = true), - manejarActualizar: handleSuccess + manejarActualizar: reloadData }) ); + // Estado para infinite scroll + let allItems = $state(data.items || []); + let currentPage = $state(data.page || 1); + let pageSize = $state(50); + let totalItems = $state(data.total || 0); + let loading = $state(false); + let hasMore = $derived(allItems.length < totalItems); + let error = $state(data.error || null); + + async function loadMore() { + if (loading || !hasMore) return; + + loading = true; + error = null; + + try { + const response = await incotermsApi.list(currentPage + 1, pageSize, searchCode, searchDesc); + + if (response.error) { + console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); + + if (response.status === 401 || response.status === 403) { + error = 'Sesión expirada. Recargando página...'; + setTimeout(() => { + window.location.reload(); + }, 2000); + } else { + error = response.error; + } + return; + } + + if (response.data?.items) { + allItems = [...allItems, ...response.data.items]; + currentPage++; + totalItems = response.data.total; + } + } catch (e) { + error = 'Error cargando más datos'; + console.error('📊 [Page] Error loading more:', e); + } finally { + loading = false; + } + } + + function reloadData() { + window.location.reload(); + } + // Filtros let searchCode = $state($page.url.searchParams.get('code') || ''); let searchDesc = $state($page.url.searchParams.get('description') || ''); @@ -33,7 +80,22 @@ function handleSearch() { if (!browser) return; clearTimeout(timeout); - timeout = setTimeout(() => { + timeout = setTimeout(async () => { + loading = true; + try { + const response = await incotermsApi.list(1, pageSize, searchCode, searchDesc); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + + // Actualizar URL silenciosamente para mantener estado const url = new URL($page.url); if (searchCode) url.searchParams.set('code', searchCode); else url.searchParams.delete('code'); @@ -41,8 +103,7 @@ if (searchDesc) url.searchParams.set('description', searchDesc); else url.searchParams.delete('description'); - url.searchParams.set('page', '1'); - goto(url, { keepFocus: true, noScroll: true }); + history.replaceState(history.state, '', url); }, 500); } @@ -52,44 +113,43 @@ } -
-
-
-

Incoterms

+
+ +
+
+

+ Incoterms +

- Catálogo de Incoterms + Catálogo de términos internacionales de comercio

-
-
- -
-
-
-
- +
+
-
- -
+ {#if error} +
+ {error} +
+ {/if} - + + +
+ Listado de Incoterms +
+ + +
+
+
+
+
+ +
Mostrando {allItems.length} de {totalItems} registros
diff --git a/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte index f615c7b3..9467a74a 100644 --- a/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte @@ -3,55 +3,30 @@ import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types'; import DataTable from '$lib/components/dashboard/reference_data/invoice_types/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/invoice_types/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/invoice_types/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/invoice_types/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Tipos de Factura', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -64,98 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await invoiceTypesApi.list(1, pageSize, undefined, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await invoiceTypesApi.list(currentPage + 1, pageSize); - + try { + const response = await invoiceTypesApi.list(currentPage + 1, pageSize, undefined, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Tipos de Factura', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
-
-
-

Tipos de Factura

+
+ +
+
+

+ Tipos de Factura +

- Gestiona los tipos de facturas del sistema + Gestiona los tipos de facturas disponibles en el sistema

-
- -
{#if error} -
+
{error}
{/if} -
- -
-
+ +
Listado de Tipos de Factura
+
+
- +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte index 1847f6e2..b77a5332 100644 --- a/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte @@ -3,56 +3,30 @@ import { materialTypesApi, type MaterialType } from '$lib/api/dashboard/reference_data/material_types'; import DataTable from '$lib/components/dashboard/reference_data/material_types/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/material_types/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/material_types/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/material_types/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Tipos de Material', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -65,116 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await materialTypesApi.list(1, pageSize, undefined, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await materialTypesApi.list(currentPage + 1, pageSize); - + try { + const response = await materialTypesApi.list(currentPage + 1, pageSize, undefined, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Tipos de Material', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
- -
-
-

Tipos de Material

+
+ +
+
+

+ Tipos de Material +

- Gestiona los tipos de materiales del sistema + Gestiona los tipos de materiales disponibles en el sistema

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Tipos de Material - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Tipos de Material
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte index 53623210..54553e47 100644 --- a/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte @@ -3,46 +3,30 @@ import { paymentMethodsApi, type PaymentMethod } from '$lib/api/dashboard/reference_data/payment_methods'; import DataTable from '$lib/components/dashboard/reference_data/payment_methods/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/payment_methods/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/payment_methods/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -55,122 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await paymentMethodsApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - try { - const response = await paymentMethodsApi.list(currentPage + 1, pageSize); - + const response = await paymentMethodsApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - if (response.data?.items) { - // Agregar los nuevos items al array existente allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Keyboard Shortcuts useShortcuts('Métodos de Pago', [ - { - key: 'Alt+Shift+N', - description: 'Nuevo Método', - action: handleCreateClick - }, - { - key: 'Alt+Shift+R', - description: 'Actualizar Lista', - action: reloadData - } + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } ]); - // Crear columnas con el callback onSuccess const columns = createColumns(handleSuccess); -
- -
-
-

Métodos de Pago

-

Gestiona las formas de pago disponibles en el sistema

+
+ +
+
+

+ Métodos de Pago +

+

+ Gestiona las formas de pago disponibles en el sistema +

+
+
+
-
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Métodos de Pago - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Métodos de Pago
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte index 15db3620..4b763276 100644 --- a/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte @@ -3,46 +3,30 @@ import { pedimentoCodesApi, type PedimentoCode } from '$lib/api/dashboard/reference_data/pedimento_codes'; import DataTable from '$lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_codes/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/pedimento_codes/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; + import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; import type { PageData } from './$types'; - import { browser } from '$app/environment'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -55,122 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await pedimentoCodesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - try { - const response = await pedimentoCodesApi.list(currentPage + 1, pageSize); - + const response = await pedimentoCodesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - if (response.data?.items) { - // Agregar los nuevos items al array existente allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Keyboard Shortcuts useShortcuts('Claves de Pedimento', [ - { - key: 'Alt+Shift+N', - description: 'Nueva Clave', - action: handleCreateClick - }, - { - key: 'Alt+Shift+R', - description: 'Actualizar Lista', - action: reloadData - } + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } ]); - // Crear columnas con el callback onSuccess const columns = createColumns(handleSuccess); -
- -
-
-

Claves de Pedimento

-

Gestiona las claves de pedimento del sistema aduanero

+
+ +
+
+

+ Claves de Pedimento +

+

+ Gestiona las claves de pedimento del sistema aduanero +

+
+
+
-
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Claves de Pedimento - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Claves de Pedimento
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte index e2222c2f..10256756 100644 --- a/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte @@ -3,46 +3,30 @@ import { pedimentoRegimensApi, type PedimentoRegimen } from '$lib/api/dashboard/reference_data/pedimento_regimens'; import DataTable from '$lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_regimens/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/pedimento_regimens/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -55,122 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await pedimentoRegimensApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - try { - const response = await pedimentoRegimensApi.list(currentPage + 1, pageSize); - + const response = await pedimentoRegimensApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - if (response.data?.items) { - // Agregar los nuevos items al array existente allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Keyboard Shortcuts useShortcuts('Regímenes', [ - { - key: 'Alt+Shift+N', - description: 'Nuevo Régimen', - action: handleCreateClick - }, - { - key: 'Alt+Shift+R', - description: 'Actualizar Lista', - action: reloadData - } + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } ]); - // Crear columnas con el callback onSuccess const columns = createColumns(handleSuccess); -
- -
-
-

Regímenes de Pedimento

-

Gestiona los regímenes aduaneros de pedimento

+
+ +
+
+

+ Regímenes +

+

+ Gestiona los regímenes aduaneros de pedimento +

+
+
+
-
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Regímenes de Pedimento - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Regímenes
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte b/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte index f944f18f..863da60e 100644 --- a/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte @@ -4,56 +4,30 @@ import { companyStore } from '$lib/stores/company.svelte'; import DataTable from '$lib/components/dashboard/reference_data/sectors/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/sectors/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/sectors/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Sectores', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -66,121 +40,128 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function getActiveCompanyId(): number | null { + const fromStore = companyStore.activeCompany?.id; + if (fromStore) return fromStore; + if (!browser) return null; + const cookie = document.cookie + .split('; ') + .find((row) => row.startsWith('active_company_id=')) + ?.split('=')[1]; + if (!cookie) return null; + const parsed = Number(cookie); + return Number.isFinite(parsed) ? parsed : null; + } + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const companyId = getActiveCompanyId(); + if (!companyId) { + error = 'No hay empresa activa seleccionada'; + return; + } + const response = await sectorsApi.list(1, pageSize, companyId, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const companyId = companyStore.activeCompany?.id; - if (!companyId) { - error = 'No hay empresa activa seleccionada'; - return; - } - const response = await sectorsApi.list(currentPage + 1, pageSize, companyId); - + try { + const companyId = getActiveCompanyId(); + if (!companyId) { + error = 'No hay empresa activa seleccionada'; + return; + } + const response = await sectorsApi.list(currentPage + 1, pageSize, companyId, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Sectores', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
- -
-
-

Sectores

+
+ +
+
+

+ Sectores +

- Gestiona los sectores económicos + Gestiona los sectores económicos del sistema

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Sectores - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Sectores
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/states/+page.svelte b/frontend/src/routes/dashboard/reference_data/states/+page.svelte index bae3c744..9153e117 100644 --- a/frontend/src/routes/dashboard/reference_data/states/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/states/+page.svelte @@ -3,29 +3,26 @@ import { statesApi, type State } from '$lib/api/dashboard/reference_data/states'; import DataTable from '$lib/components/dashboard/reference_data/states/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/states/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/states/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; + import { Input } from '$lib/components/ui/input'; import type { PageData } from './$types'; import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/states/list'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { page } from '$app/stores'; + import { RefreshCw } from 'lucide-svelte'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Estados', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); + // Keyboard Shortcuts + useShortcuts('Estados', [ + { + key: 'Alt+Shift+R', + description: 'Actualizar Lista', + action: reloadData + } + ]); // Sincronizar token de cookies a localStorage al montar el componente onMount(() => { @@ -65,6 +62,34 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await statesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; @@ -72,7 +97,7 @@ error = null; try { - const response = await statesApi.list(currentPage + 1, pageSize); + const response = await statesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); @@ -109,10 +134,6 @@ window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { // Recargar datos después de crear/editar/eliminar reloadData(); @@ -122,59 +143,35 @@ const columns = createColumns(handleSuccess); -
- -
-
-

Estados

+
+ +
+
+

+ Estados +

Gestiona los estados y sus claves de identificación

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Estados - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Estados
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte index 511ac0e4..15541683 100644 --- a/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte @@ -3,56 +3,30 @@ import { transportModesApi, type TransportMode } from '$lib/api/dashboard/reference_data/transport_modes'; import DataTable from '$lib/components/dashboard/reference_data/transport_modes/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/transport_modes/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/transport_modes/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/transport_modes/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; - import { Plus, RefreshCw } from 'lucide-svelte'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Modos de Transporte', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -65,116 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await transportModesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await transportModesApi.list(currentPage + 1, pageSize); - + try { + const response = await transportModesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Modos de Transporte', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
- -
-
-

Modos de Transporte

+
+ +
+
+

+ Modos de Transporte +

- Gestiona los modos de transporte disponibles + Gestiona los modos de transporte disponibles en el sistema

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Modos de Transporte - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Modos de Transporte
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte index 6506fd52..dd9fcc29 100644 --- a/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte @@ -3,56 +3,30 @@ import { transportTypesApi, type TransportType } from '$lib/api/dashboard/reference_data/transport_types'; import DataTable from '$lib/components/dashboard/reference_data/transport_types/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/transport_types/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/transport_types/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; - import type { PageData } from './$types'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/transport_types/list'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Tipos de Transporte', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -65,116 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await transportTypesApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await transportTypesApi.list(currentPage + 1, pageSize); - + try { + const response = await transportTypesApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Tipos de Transporte', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
- -
-
-

Tipos de Transporte

+
+ +
+
+

+ Tipos de Transporte +

Gestiona los tipos de transporte según código SAT

- +
+ +
- {#if error} - - - Error - {error} - - +
+ {error} +
{/if} - - - -
-
- Listado de Tipos de Transporte - - Mostrando {allItems.length} de {totalItems} registros - -
- -
-
- - - - + +
Listado de Tipos de Transporte
+
-
- - +
Mostrando {allItems.length} de {totalItems} registros
+
diff --git a/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte index 59c8a0fc..03389645 100644 --- a/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte @@ -3,55 +3,30 @@ import { valuationMethodsApi, type ValuationMethod } from '$lib/api/dashboard/reference_data/valuation_methods'; import DataTable from '$lib/components/dashboard/reference_data/valuation_methods/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/reference_data/valuation_methods/columns.js'; - import CreateEditDialog from '$lib/components/dashboard/reference_data/valuation_methods/create-edit-dialog.svelte'; + import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; - import { Plus, RefreshCw } from 'lucide-svelte'; - import { useShortcuts } from '$lib/hooks/use-shortcuts'; - import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/valuation_methods/list'; - import type { PageData } from './$types'; + import { Input } from '$lib/components/ui/input'; + import { page } from '$app/stores'; import { browser } from '$app/environment'; + import { RefreshCw } from 'lucide-svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import type { PageData } from './$types'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); - - // Estado para el diálogo de crear - let showCreateDialog = $state(false); - // Atajos - useShortcuts( - 'Lista Métodos de Valoración', - obtenerAtajosLista({ - manejarNuevo: () => (showCreateDialog = true), - manejarActualizar: reloadData - }) - ); - - // Sincronizar token de cookies a localStorage al montar el componente + // Sincronizar token de cookies onMount(() => { if (browser) { - // Función para obtener el valor de una cookie const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; - - // Verificar si hay token en las cookies const cookieToken = getCookie('access_token'); const localToken = localStorage.getItem('access_token'); - - if (cookieToken && cookieToken !== localToken) { - localStorage.setItem('access_token', cookieToken); - } - - // También sincronizar refresh_token si existe - const cookieRefreshToken = getCookie('refresh_token'); - const localRefreshToken = localStorage.getItem('refresh_token'); - - if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) { - localStorage.setItem('refresh_token', cookieRefreshToken); - } + if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken); } }); @@ -64,98 +39,105 @@ let hasMore = $derived(allItems.length < totalItems); let error = $state(data.error || null); + // Filtros + let searchQuery = $state($page.url.searchParams.get('search') || ''); + let timeout: ReturnType; + + function handleSearch() { + if (!browser) return; + clearTimeout(timeout); + timeout = setTimeout(async () => { + loading = true; + try { + const response = await valuationMethodsApi.list(1, pageSize, searchQuery); + if (!response.error && response.data) { + allItems = response.data.items; + currentPage = 1; + totalItems = response.data.total; + } + } catch (e) { + console.error('Error aplicando filtros:', e); + } finally { + loading = false; + } + const url = new URL($page.url); + if (searchQuery) url.searchParams.set('search', searchQuery); + else url.searchParams.delete('search'); + history.replaceState(history.state, '', url); + }, 500); + } + async function loadMore() { if (loading || !hasMore) return; - loading = true; error = null; - - try { - const response = await valuationMethodsApi.list(currentPage + 1, pageSize); - + try { + const response = await valuationMethodsApi.list(currentPage + 1, pageSize, searchQuery); if (response.error) { - console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status); - - // Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico if (response.status === 401 || response.status === 403) { error = 'Sesión expirada. Recargando página...'; - // Recargar automáticamente después de 2 segundos - setTimeout(() => { - window.location.reload(); - }, 2000); + setTimeout(() => window.location.reload(), 2000); } else { error = response.error; } return; } - - if (response.data?.items) { - // Agregar los nuevos items al array existente + if (response.data?.items) { allItems = [...allItems, ...response.data.items]; currentPage++; totalItems = response.data.total; } } catch (e) { error = 'Error cargando más datos'; - console.error('📊 [Page] Error loading more:', e); } finally { loading = false; } } function reloadData() { - // Reset y recargar desde el principio window.location.reload(); } - function handleCreateClick() { - showCreateDialog = true; - } - function handleSuccess() { - // Recargar datos después de crear/editar/eliminar reloadData(); } - // Crear columnas con el callback onSuccess + useShortcuts('Métodos de Valoración', [ + { key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData } + ]); + const columns = createColumns(handleSuccess); -
-
-
-

Métodos de Valoración

+
+ +
+
+

+ Métodos de Valoración +

- Gestiona los métodos de valoración aduanera + Gestiona los métodos de valoración aduanera autorizados

-
- -
{#if error} -
+
{error}
{/if} -
- -
-
+ +
Listado de Métodos de Valoración
+
+
- +
Mostrando {allItems.length} de {totalItems} registros
+