feat: Introduce a new TariffFractionSelector component, refactor tariff fraction handling across frontend forms and backend services, and add a company amendment database migration.
This commit is contained in:
@@ -48,6 +48,9 @@ class TariffFractionResponseDTO(BaseModel):
|
||||
umt: Optional[str] = None
|
||||
adv_impo: Optional[str] = None
|
||||
adv_expo: Optional[str] = None
|
||||
dof: Optional[str] = None
|
||||
aplica_ieps: Optional[str] = None
|
||||
um_code: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -38,7 +38,11 @@ async def list_tariff_fractions(
|
||||
if search:
|
||||
filters["search"] = search
|
||||
|
||||
items, total = TariffFractionService.get_all(
|
||||
# Updated to async call with Sitar integration
|
||||
# WARNING: Using async def with blocking DB dependency (Session) run in threadpool by FastAPI.
|
||||
# Service.get_all calls Sitar (async) or DB (sync).
|
||||
# This should be fine.
|
||||
items, total = await TariffFractionService.get_all(
|
||||
db, skip, page_size, filters
|
||||
)
|
||||
|
||||
@@ -55,7 +59,7 @@ async def list_tariff_fractions(
|
||||
"/{tariff_fraction_id}",
|
||||
response_model=TariffFractionResponseDTO,
|
||||
summary="Get Tariff Fraction by ID",
|
||||
description="Get a specific tariff fraction by ID",
|
||||
description="Get a specific tariff fraction by ID (Lookups in Local DB for legacy compatibility)",
|
||||
)
|
||||
async def get_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
@@ -68,55 +72,3 @@ async def get_tariff_fraction(
|
||||
raise HTTPException(status_code=404, detail="Tariff fraction not found")
|
||||
return TariffFractionResponseDTO.model_validate(item)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=TariffFractionResponseDTO,
|
||||
summary="Create Tariff Fraction",
|
||||
description="Create a new tariff fraction (admin only)",
|
||||
status_code=201,
|
||||
)
|
||||
async def create_tariff_fraction(
|
||||
data: TariffFractionCreateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
item = TariffFractionService.create(db, data)
|
||||
return TariffFractionResponseDTO.model_validate(item)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{tariff_fraction_id}",
|
||||
response_model=TariffFractionResponseDTO,
|
||||
summary="Update Tariff Fraction",
|
||||
description="Update an existing tariff fraction (admin only)",
|
||||
)
|
||||
async def update_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
data: TariffFractionUpdateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
item = TariffFractionService.update(db, tariff_fraction_id, data)
|
||||
if not item:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="Tariff fraction not found")
|
||||
return TariffFractionResponseDTO.model_validate(item)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{tariff_fraction_id}",
|
||||
summary="Delete Tariff Fraction",
|
||||
description="Delete a tariff fraction (admin only)",
|
||||
status_code=204,
|
||||
)
|
||||
async def delete_tariff_fraction(
|
||||
tariff_fraction_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
success = TariffFractionService.delete(db, tariff_fraction_id)
|
||||
if not success:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="Tariff fraction not found")
|
||||
|
||||
|
||||
@@ -7,26 +7,165 @@ from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import zlib
|
||||
import logging
|
||||
|
||||
from .models import TariffFraction
|
||||
from .dto import TariffFractionCreateDTO, TariffFractionUpdateDTO
|
||||
from api.v1.modules.sitar.fracciones.service import FraccionesService
|
||||
from api.v1.modules.sitar.fracciones.schemas import FraccionesResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TariffFractionMapper:
|
||||
"""Helper to map Sitar responses to Local domain objects"""
|
||||
|
||||
@staticmethod
|
||||
def to_domain(fraccion: FraccionesResponse) -> TariffFraction:
|
||||
# Generate ID: Use SYSID if available, else composite hash of code + nico
|
||||
if fraccion.SYSID:
|
||||
fake_id = fraccion.SYSID
|
||||
else:
|
||||
# Composite key for uniqueness if SYSID missing
|
||||
unique_str = f"{fraccion.FRACCION}-{fraccion.NICO}"
|
||||
fake_id = zlib.crc32(unique_str.encode('utf-8'))
|
||||
|
||||
# UX Enhauncement: Sitar API returns empty strings for some fields.
|
||||
# We fill them with fallbacks so the frontend table isn't 90% empty.
|
||||
code_val = fraccion.FRACCION
|
||||
|
||||
# Formatting Logic: if FRACCIONPUNTO is empty, try to format code_val
|
||||
formatted_fraction = code_val
|
||||
if fraccion.FRACCIONPUNTO:
|
||||
formatted_fraction = fraccion.FRACCIONPUNTO
|
||||
elif code_val and code_val.isdigit() and len(code_val) == 8:
|
||||
# Standard 8 digit format: XX.XX.XX.XX
|
||||
formatted_fraction = f"{code_val[:2]}.{code_val[2:4]}.{code_val[4:6]}.{code_val[6:]}"
|
||||
elif code_val and code_val.isdigit() and len(code_val) == 6:
|
||||
# 6 digit (subheading): XX.XX.XX
|
||||
formatted_fraction = f"{code_val[:2]}.{code_val[2:4]}.{code_val[4:]}"
|
||||
|
||||
fraction_val = formatted_fraction
|
||||
description_val = fraccion.DESCRIPCION if fraccion.DESCRIPCION else "(Sin descripción)"
|
||||
|
||||
tf = TariffFraction(
|
||||
id=fake_id,
|
||||
code=code_val,
|
||||
fraction=fraction_val,
|
||||
description=description_val,
|
||||
nico=fraccion.NICO,
|
||||
# MAP CHANGE: UMT now maps to Abbreviation (e.g., Pza, Kg)
|
||||
umt=fraccion.UMABREVIACION,
|
||||
adv_impo=fraccion.ADVIMPOTXT,
|
||||
adv_expo=fraccion.ADVEXPOTXT
|
||||
)
|
||||
# Dynamically attach non-model attributes for DTO
|
||||
tf.dof = fraccion.DOF
|
||||
tf.aplica_ieps = fraccion.APLICAIEPS
|
||||
# MAP CHANGE: New field for the numeric code (e.g., 01, 06)
|
||||
tf.um_code = fraccion.UMCLAVE
|
||||
|
||||
return tf
|
||||
|
||||
|
||||
class TariffFractionService:
|
||||
"""Service para gestionar fracciones arancelarias (catálogo global)"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
async def get_all(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[TariffFraction], int]:
|
||||
"""Obtiene todas las fracciones arancelarias con filtros opcionales"""
|
||||
"""
|
||||
Obtiene fracciones arancelarias.
|
||||
Estrategia: Sitar API -> Fallback Local DB
|
||||
"""
|
||||
|
||||
# 1. Try Sitar API
|
||||
try:
|
||||
sitar_service = FraccionesService.get_instance()
|
||||
|
||||
# Map filters
|
||||
sitar_fraccion = None
|
||||
sitar_nico = None
|
||||
has_filters = False
|
||||
|
||||
if filters:
|
||||
if filters.get("search"):
|
||||
term = filters["search"]
|
||||
# Heuristic: if search starts with digit (after removing dots), treat as code/fraccion/nico
|
||||
# This covers "0101", "01.01", "020691A"
|
||||
clean_term = term.replace(".", "")
|
||||
if clean_term and clean_term[0].isdigit():
|
||||
sitar_fraccion = clean_term
|
||||
has_filters = True
|
||||
else:
|
||||
# Attempt description search via API first
|
||||
logger.info(f"Search term '{term}' identified as text. Attempting API description search.")
|
||||
pass
|
||||
|
||||
if filters.get("code"):
|
||||
sitar_fraccion = filters["code"]
|
||||
has_filters = True
|
||||
if filters.get("fraction"):
|
||||
sitar_fraccion = filters["fraction"]
|
||||
has_filters = True
|
||||
if filters.get("nico"):
|
||||
sitar_nico = filters["nico"]
|
||||
has_filters = True
|
||||
|
||||
# Determine description filter
|
||||
sitar_description = None
|
||||
# Only use description if we didn't use it as code above
|
||||
if filters and filters.get("search"):
|
||||
clean_term = filters["search"].replace(".", "")
|
||||
if not (clean_term and clean_term[0].isdigit()):
|
||||
sitar_description = filters["search"]
|
||||
has_filters = True
|
||||
|
||||
# Note: Sitar search might not return total count.
|
||||
# We fetch page items. Pagination might be tricky if Sitar doesn't return total.
|
||||
# Assuming Sitar returns a list.
|
||||
sitar_items = await sitar_service.search(
|
||||
fraccion=sitar_fraccion,
|
||||
nico=sitar_nico,
|
||||
description=sitar_description,
|
||||
nivel=5, # User requested filtering by level 5
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# STRICT API USAGE:
|
||||
# We do NOT fallback to local DB on empty list, as user requested strict API consumption.
|
||||
# We also do NOT attempt enrichment as codes mismatch (API uses '010191A' vs Local '01012101').
|
||||
|
||||
# Map items
|
||||
items = [TariffFractionMapper.to_domain(item) for item in sitar_items]
|
||||
|
||||
# Estimate total (Sitar service doesn't return total currently)
|
||||
# If we got full limit, assume there are more.
|
||||
total = len(items) + skip
|
||||
if len(items) == limit:
|
||||
total += 1 # Indicate more pages
|
||||
|
||||
return items, total
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching from Sitar API: {e}")
|
||||
# STRICT API USAGE: Propagate error, do NOT fallback to local DB.
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
async def _get_all_local_async(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[TariffFraction], int]:
|
||||
"""Lógica original de consulta local"""
|
||||
query = db.query(TariffFraction)
|
||||
|
||||
# Aplicar filtros
|
||||
@@ -64,21 +203,24 @@ class TariffFractionService:
|
||||
db: Session,
|
||||
tariff_fraction_id: int,
|
||||
) -> Optional[TariffFraction]:
|
||||
"""Obtiene una fracción arancelaria por ID"""
|
||||
|
||||
"""
|
||||
Obtiene por ID.
|
||||
Como Sitar no usa estos IDs, consultamos Local DB directamente para compatibilidad legacy.
|
||||
Si se necesitara obtener detalle de Sitar, se requeriría otro identificador (Code).
|
||||
"""
|
||||
return (
|
||||
db.query(TariffFraction)
|
||||
.filter(TariffFraction.id == tariff_fraction_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
# WRITE OPERATIONS - DEPRECATED / LOCAL ONLY (Optional: Remove or Keep for Fallback Maintenance)
|
||||
|
||||
@staticmethod
|
||||
def get_by_code(
|
||||
db: Session,
|
||||
code: str,
|
||||
) -> Optional[TariffFraction]:
|
||||
"""Obtiene una fracción arancelaria por código"""
|
||||
|
||||
return (
|
||||
db.query(TariffFraction)
|
||||
.filter(TariffFraction.code == code)
|
||||
@@ -90,8 +232,6 @@ class TariffFractionService:
|
||||
db: Session,
|
||||
tariff_fraction_data: TariffFractionCreateDTO,
|
||||
) -> TariffFraction:
|
||||
"""Crea una nueva fracción arancelaria"""
|
||||
|
||||
try:
|
||||
tariff_fraction = TariffFraction(
|
||||
**tariff_fraction_data.model_dump(),
|
||||
@@ -114,8 +254,6 @@ class TariffFractionService:
|
||||
tariff_fraction_id: int,
|
||||
tariff_fraction_data: TariffFractionUpdateDTO,
|
||||
) -> Optional[TariffFraction]:
|
||||
"""Actualiza una fracción arancelaria existente"""
|
||||
|
||||
tariff_fraction = TariffFractionService.get_by_id(
|
||||
db, tariff_fraction_id
|
||||
)
|
||||
@@ -144,8 +282,6 @@ class TariffFractionService:
|
||||
db: Session,
|
||||
tariff_fraction_id: int,
|
||||
) -> bool:
|
||||
"""Elimina una fracción arancelaria"""
|
||||
|
||||
tariff_fraction = TariffFractionService.get_by_id(
|
||||
db, tariff_fraction_id
|
||||
)
|
||||
|
||||
@@ -17,21 +17,8 @@ from .dto import (
|
||||
)
|
||||
from .service import USTariffFractionService
|
||||
|
||||
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
|
||||
base_router = TenantCRUDRoutes(
|
||||
service=USTariffFractionService,
|
||||
create_schema=USTariffFractionCreateDTO,
|
||||
update_schema=USTariffFractionUpdateDTO,
|
||||
response_schema=USTariffFractionResponseDTO,
|
||||
prefix="/us-tariff-fractions",
|
||||
tags=["a76 / general catalogs / us tariff fractions"],
|
||||
resource_name="USTariffFraction",
|
||||
id_name="us_tariff_fraction_id",
|
||||
enable_list=False, # Disable default list, we'll add custom one
|
||||
enable_filters=False,
|
||||
default_page_size=50,
|
||||
max_page_size=10000,
|
||||
)
|
||||
# Create base router with generic CRUD routes - REMOVED strictly read-only from Sitar
|
||||
# Writes are disabled at API level, but Service still supports fallback writes if needed internally
|
||||
|
||||
router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
|
||||
|
||||
@@ -57,7 +44,8 @@ async def list_us_tariff_fractions(
|
||||
if search:
|
||||
filters["search"] = search
|
||||
|
||||
items, total = USTariffFractionService.get_all(
|
||||
# Updated to async call with Sitar integration
|
||||
items, total = await USTariffFractionService.get_all(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
)
|
||||
|
||||
@@ -69,5 +57,23 @@ async def list_us_tariff_fractions(
|
||||
"pages": (total + page_size - 1) // page_size,
|
||||
}
|
||||
|
||||
# Include other CRUD routes from base router
|
||||
router.include_router(base_router.router)
|
||||
|
||||
@router.get(
|
||||
"/{us_tariff_fraction_id}",
|
||||
response_model=USTariffFractionResponseDTO,
|
||||
summary="Get US Tariff Fraction by ID",
|
||||
description="Get a specific US tariff fraction by ID (Lookups in Local DB for legacy compatibility)",
|
||||
)
|
||||
async def get_us_tariff_fraction(
|
||||
us_tariff_fraction_id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
item = USTariffFractionService.get_by_id(db, tenant_id, company_id, us_tariff_fraction_id)
|
||||
if not item:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="US Tariff fraction not found")
|
||||
return USTariffFractionResponseDTO.model_validate(item)
|
||||
|
||||
@@ -6,19 +6,68 @@ from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import zlib
|
||||
import logging
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from .models import USTariffFraction
|
||||
from .dto import USTariffFractionCreateDTO, USTariffFractionUpdateDTO
|
||||
from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService
|
||||
from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class USTariffFractionMapper:
|
||||
"""Helper to map Sitar USA responses to Local domain objects"""
|
||||
|
||||
@staticmethod
|
||||
def to_domain(fraccion: FraccionesUSAResponse, tenant_id: int, company_id: int) -> USTariffFraction:
|
||||
# Generate a deterministic numeric ID based on the unique code
|
||||
# We use CRC32 to get a consistent integer implementation-independent
|
||||
fake_id = zlib.crc32((fraccion.FRACCION_SIN_PUNTO or "").encode('utf-8'))
|
||||
|
||||
# Parse numeric values safely
|
||||
ad_valorem = None
|
||||
if fraccion.TARIFA1:
|
||||
try:
|
||||
# Extract numbers from string like "5.2%" or similar if present
|
||||
# Assuming TARIFA1 might be clean number or percentage string
|
||||
clean_val = re.sub(r'[^\d.]', '', str(fraccion.TARIFA1))
|
||||
if clean_val:
|
||||
ad_valorem = Decimal(clean_val)
|
||||
except:
|
||||
pass
|
||||
|
||||
fixed_cost = None
|
||||
if fraccion.ESPECIFICO:
|
||||
try:
|
||||
clean_val = re.sub(r'[^\d.]', '', str(fraccion.ESPECIFICO))
|
||||
if clean_val:
|
||||
fixed_cost = Decimal(clean_val)
|
||||
except:
|
||||
pass
|
||||
|
||||
return USTariffFraction(
|
||||
id=fake_id, # Updated to use fake_id instead of sitar consecutive if needed, or consistent hash
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
code=fraccion.FRACCION_SIN_PUNTO or "",
|
||||
prefix=None, # Not mapped from Sitar response currently
|
||||
type_code=None,
|
||||
ad_valorem=ad_valorem,
|
||||
fixed_cost=fixed_cost,
|
||||
unit_of_measure=fraccion.UNIDADCANTIDAD,
|
||||
description=fraccion.DESCRIPCION
|
||||
)
|
||||
|
||||
|
||||
class USTariffFractionService:
|
||||
"""Service para gestionar fracciones arancelarias americanas"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
async def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
@@ -26,8 +75,60 @@ class USTariffFractionService:
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[USTariffFraction], int]:
|
||||
"""Obtiene todas las fracciones arancelarias americanas con filtros opcionales"""
|
||||
"""
|
||||
Obtiene todas las fracciones arancelarias americanas con filtros opcionales.
|
||||
Estrategia: Sitar API -> Fallback Local DB
|
||||
"""
|
||||
|
||||
# 1. Try Sitar API
|
||||
try:
|
||||
sitar_service = FraccionesUSAService.get_instance()
|
||||
|
||||
sitar_fraccion = None
|
||||
has_filters = False
|
||||
|
||||
if filters and filters.get("search"):
|
||||
term = filters["search"]
|
||||
# Sitar only filters by fraction code
|
||||
if term.replace(".", "").isdigit():
|
||||
sitar_fraccion = term
|
||||
has_filters = True
|
||||
|
||||
sitar_items = await sitar_service.search(
|
||||
fraccion=sitar_fraccion,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# If Sitar returns empty list AND we didn't have specific filters, attempt fallback
|
||||
if not sitar_items and not has_filters:
|
||||
logger.warning("Sitar return empty list for USA broad query. Attempting fallback to local DB.")
|
||||
return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters)
|
||||
|
||||
# Map items
|
||||
items = [USTariffFractionMapper.to_domain(item, tenant_id, company_id) for item in sitar_items]
|
||||
|
||||
# Estimate total
|
||||
total = len(items) + skip
|
||||
if len(items) == limit:
|
||||
total += 1
|
||||
|
||||
return items, total
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching USA Fractions from Sitar API, falling back to local DB: {e}")
|
||||
return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters)
|
||||
|
||||
@staticmethod
|
||||
def _get_all_local(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[USTariffFraction], int]:
|
||||
"""Lógica original de consulta local"""
|
||||
query = db.query(USTariffFraction).filter(
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
@@ -53,7 +154,10 @@ class USTariffFractionService:
|
||||
def get_by_id(
|
||||
db: Session, tenant_id: int, company_id: int, fraction_id: int
|
||||
) -> Optional[USTariffFraction]:
|
||||
"""Obtiene una fracción arancelaria americana por ID"""
|
||||
"""
|
||||
Obtiene por ID.
|
||||
Legacy: Consulta Local DB.
|
||||
"""
|
||||
return (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
@@ -64,6 +168,8 @@ class USTariffFractionService:
|
||||
.first()
|
||||
)
|
||||
|
||||
# WRITE OPERATIONS - DEPRECATED / LOCAL ONLY
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
@@ -71,7 +177,6 @@ class USTariffFractionService:
|
||||
company_id: int,
|
||||
fraction_data: USTariffFractionCreateDTO,
|
||||
) -> USTariffFraction:
|
||||
"""Crea una nueva fracción arancelaria americana"""
|
||||
try:
|
||||
db_fraction = USTariffFraction(
|
||||
tenant_id=tenant_id,
|
||||
@@ -98,7 +203,6 @@ class USTariffFractionService:
|
||||
fraction_id: int,
|
||||
fraction_data: USTariffFractionUpdateDTO,
|
||||
) -> Optional[USTariffFraction]:
|
||||
"""Actualiza una fracción arancelaria americana existente"""
|
||||
db_fraction = USTariffFractionService.get_by_id(
|
||||
db, tenant_id, company_id, fraction_id
|
||||
)
|
||||
@@ -117,7 +221,6 @@ class USTariffFractionService:
|
||||
def delete(
|
||||
db: Session, tenant_id: int, company_id: int, fraction_id: int
|
||||
) -> bool:
|
||||
"""Elimina una fracción arancelaria americana"""
|
||||
db_fraction = USTariffFractionService.get_by_id(
|
||||
db, tenant_id, company_id, fraction_id
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ class SitarAPIBaseService:
|
||||
self.base_url = os.getenv("SITAR_API_URL")
|
||||
self.username = os.getenv("SITAR_API_USER")
|
||||
self.password = os.getenv("SITAR_API_PASSWORD")
|
||||
self.timeout = 10.0
|
||||
self.timeout = 30.0
|
||||
|
||||
if not all([self.base_url, self.username, self.password]):
|
||||
raise ValueError(
|
||||
@@ -101,4 +101,20 @@ class SitarAPIBaseService:
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# DEBUG LOGGING for SITAR inspection
|
||||
if "fracciones" in url:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"SITAR API Response Headers for {url}: {dict(response.headers)}")
|
||||
try:
|
||||
data = response.json()
|
||||
if isinstance(data, dict):
|
||||
logger.info(f"SITAR API Response Body Keys: {list(data.keys())}")
|
||||
elif isinstance(data, list) and len(data) > 0:
|
||||
logger.info(f"SITAR API Response List Item Keys: {list(data[0].keys())}")
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return response.json()
|
||||
|
||||
@@ -33,6 +33,7 @@ class FraccionesResponse(BaseModel):
|
||||
APLICAIEPS: Optional[str] = Field(None, max_length=1)
|
||||
NIVEL: Optional[int] = None
|
||||
NICO: Optional[str] = Field(None, max_length=14)
|
||||
SYSID: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -21,6 +21,8 @@ class FraccionesService(SitarAPIBaseService):
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
nivel: Optional[int] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[FraccionesResponse]:
|
||||
@@ -30,6 +32,10 @@ class FraccionesService(SitarAPIBaseService):
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
if description:
|
||||
params["descripcion"] = description
|
||||
if nivel is not None:
|
||||
params["nivel"] = nivel
|
||||
|
||||
data = await self._make_request("GET", "/api/v1/fracciones/", params=params)
|
||||
return [FraccionesResponse(**item) for item in data]
|
||||
|
||||
@@ -11,8 +11,10 @@ export interface TariffFraction {
|
||||
umt: string | null;
|
||||
adv_impo: string | null;
|
||||
adv_expo: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
dof: string | null;
|
||||
aplica_ieps: string | null;
|
||||
um_code: string | null;
|
||||
}
|
||||
|
||||
export interface TariffFractionCreate {
|
||||
|
||||
@@ -143,16 +143,30 @@
|
||||
let showUnitDialog = $state(false);
|
||||
let searchUnit = $state('');
|
||||
|
||||
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
|
||||
|
||||
let showFractionDialog = $state(false);
|
||||
let tariffFractions = $state<TariffFraction[]>([]);
|
||||
let searchFraction = $state('');
|
||||
let currentPage = $state(1);
|
||||
let totalFractions = $state(0);
|
||||
let hasMoreFractions = $state(true);
|
||||
let isLoadingFractions = $state(false);
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
// Removing inline tariff fraction state
|
||||
// let tariffFractions = ...
|
||||
|
||||
function openFractionSearch() {
|
||||
showFractionDialog = true;
|
||||
}
|
||||
|
||||
let showUSFractionDialog = $state(false);
|
||||
// ...
|
||||
|
||||
// loadFractions removed
|
||||
|
||||
function selectFraction(fraction: TariffFraction) {
|
||||
formData.fraction = fraction.fraction;
|
||||
formData.fraction_umt = (fraction.umt ?? '') as string;
|
||||
formData.fraction_uma_key = (fraction.nico ?? '') as string;
|
||||
// Actualizar tarifa de importación
|
||||
formData.import_tariff_code = fraction.fraction || '';
|
||||
formData.import_tariff_type = (fraction.umt ?? '') as string;
|
||||
showFractionDialog = false;
|
||||
}
|
||||
let usTariffFractions = $state<USTariffFraction[]>([]);
|
||||
let searchUSFraction = $state('');
|
||||
let currentUSPage = $state(1);
|
||||
@@ -262,56 +276,6 @@
|
||||
searchUnit = '';
|
||||
}
|
||||
|
||||
async function openFractionSearch() {
|
||||
showFractionDialog = true;
|
||||
searchFraction = '';
|
||||
tariffFractions = [];
|
||||
currentPage = 1;
|
||||
totalFractions = 0;
|
||||
hasMoreFractions = true;
|
||||
await loadFractions('', 1);
|
||||
}
|
||||
|
||||
async function loadFractions(search: string, page: number = currentPage) {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId || isLoadingFractions) return;
|
||||
|
||||
isLoadingFractions = true;
|
||||
try {
|
||||
const filters = search ? { search } : {};
|
||||
const pageSize = 100;
|
||||
|
||||
const response = await getTariffFractions(page, pageSize, companyId, filters);
|
||||
|
||||
if (response.data) {
|
||||
if (page === 1) {
|
||||
tariffFractions = [...response.data.items];
|
||||
} else {
|
||||
tariffFractions = [...tariffFractions, ...response.data.items];
|
||||
}
|
||||
|
||||
totalFractions = response.data.total;
|
||||
currentPage = page;
|
||||
hasMoreFractions = tariffFractions.length < response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cargando fracciones arancelarias:', error);
|
||||
} finally {
|
||||
isLoadingFractions = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectFraction(fraction: TariffFraction) {
|
||||
formData.fraction = fraction.fraction;
|
||||
formData.fraction_umt = (fraction.umt ?? '') as string;
|
||||
formData.fraction_uma_key = (fraction.nico ?? '') as string;
|
||||
// Actualizar tarifa de importación
|
||||
formData.import_tariff_code = fraction.fraction || '';
|
||||
formData.import_tariff_type = (fraction.umt ?? '') as string;
|
||||
showFractionDialog = false;
|
||||
searchFraction = '';
|
||||
}
|
||||
|
||||
async function openUSFractionSearch() {
|
||||
showUSFractionDialog = true;
|
||||
searchUSFraction = '';
|
||||
@@ -565,7 +529,7 @@
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Clase y Requiere Revisión Física -->
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label for="class_code" class="font-bold">
|
||||
Clase: <span class="text-red-500">*</span>
|
||||
@@ -588,7 +552,7 @@
|
||||
onblur={() => validateField('class_code')}
|
||||
/>
|
||||
{#if validationErrors.class_code}
|
||||
<p class="text-sm text-red-500 mt-1">{validationErrors.class_code}</p>
|
||||
<p class="mt-1 text-sm text-red-500">{validationErrors.class_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -596,7 +560,7 @@
|
||||
<Label for="material_key" class="font-bold">
|
||||
Tipo de Activo Fijo: <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
id="material_key"
|
||||
bind:value={formData.material_key}
|
||||
@@ -610,12 +574,12 @@
|
||||
<Button type="button" variant="outline" size="icon" onclick={openMaterialSearch}>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
<span class="text-blue-600 hover:underline cursor-pointer text-sm">
|
||||
<span class="cursor-pointer text-sm text-blue-600 hover:underline">
|
||||
{formData.material_description || ''}
|
||||
</span>
|
||||
</div>
|
||||
{#if validationErrors.material_key}
|
||||
<p class="text-sm text-red-500 mt-1">{validationErrors.material_key}</p>
|
||||
<p class="mt-1 text-sm text-red-500">{validationErrors.material_key}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -635,7 +599,7 @@
|
||||
onblur={() => validateField('description_es')}
|
||||
/>
|
||||
{#if validationErrors.description_es}
|
||||
<p class="text-sm text-red-500 mt-1">{validationErrors.description_es}</p>
|
||||
<p class="mt-1 text-sm text-red-500">{validationErrors.description_es}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -651,12 +615,12 @@
|
||||
</div>
|
||||
|
||||
<!-- U.M. Comercial y Fracción Arancelaria -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="unit_of_measure" class="font-bold">
|
||||
U.M. Comercial: <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
id="unit_of_measure"
|
||||
bind:value={formData.unit_of_measure}
|
||||
@@ -670,12 +634,12 @@
|
||||
<Button type="button" variant="outline" size="icon" onclick={openUnitOfMeasureSearch}>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
<span class="text-blue-600 hover:underline cursor-pointer text-sm">
|
||||
<span class="cursor-pointer text-sm text-blue-600 hover:underline">
|
||||
{formData.unit_of_measure_description || ''}
|
||||
</span>
|
||||
</div>
|
||||
{#if validationErrors.unit_of_measure}
|
||||
<p class="text-sm text-red-500 mt-1">{validationErrors.unit_of_measure}</p>
|
||||
<p class="mt-1 text-sm text-red-500">{validationErrors.unit_of_measure}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -683,7 +647,7 @@
|
||||
<Label for="fraction" class="font-bold">
|
||||
Fracción Arancelaria: <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
id="fraction"
|
||||
bind:value={formData.fraction}
|
||||
@@ -699,16 +663,16 @@
|
||||
</Button>
|
||||
</div>
|
||||
{#if validationErrors.fraction}
|
||||
<p class="text-sm text-red-500 mt-1">{validationErrors.fraction}</p>
|
||||
<p class="mt-1 text-sm text-red-500">{validationErrors.fraction}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fracción Americana y Tasa Depreciación -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="us_fraction">Fracción Americana:</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
id="us_fraction"
|
||||
bind:value={formData.us_fraction}
|
||||
@@ -724,7 +688,7 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="annual_depreciation_rate">Tasa Anual de Depreciación:</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
id="annual_depreciation_rate"
|
||||
type="number"
|
||||
@@ -742,10 +706,10 @@
|
||||
</div>
|
||||
|
||||
<!-- FDA, Carta Porte y ECCN -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="fda_key">Clave FDA:</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input id="fda_key" bind:value={formData.fda_key} placeholder="Clave FDA" class="flex-1" />
|
||||
<Button type="button" variant="outline" size="icon" onclick={openFDASearch}>
|
||||
<Folder class="h-4 w-4" />
|
||||
@@ -755,7 +719,7 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="carta_porte_code">Carta Porte:</Label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
id="carta_porte_code"
|
||||
bind:value={formData.carta_porte_code}
|
||||
@@ -800,9 +764,9 @@
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-96 overflow-auto">
|
||||
<div class="max-h-96 overflow-auto rounded-md border">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold">Clave</th>
|
||||
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
|
||||
@@ -811,7 +775,7 @@
|
||||
<tbody>
|
||||
{#each filteredMaterialTypes as material (material.key)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => selectMaterial(material)}
|
||||
>
|
||||
<td class="px-4 py-2">{material.key}</td>
|
||||
@@ -844,9 +808,9 @@
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-96 overflow-auto">
|
||||
<div class="max-h-96 overflow-auto rounded-md border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold">Código</th>
|
||||
<th class="px-3 py-2 text-left font-semibold">Descripción</th>
|
||||
@@ -857,7 +821,7 @@
|
||||
<tbody>
|
||||
{#each filteredUnits as unit (unit.code)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => selectUnit(unit)}
|
||||
>
|
||||
<td class="px-3 py-2">{unit.code}</td>
|
||||
@@ -877,66 +841,11 @@
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Dialog: Buscar fracción arancelaria -->
|
||||
<Dialog.Root bind:open={showFractionDialog}>
|
||||
<Dialog.Content class="max-w-4xl w-full">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>CATALOGO DE FRACCIONES SITAR - SCAII</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="search_fraction">Buscando:</Label>
|
||||
<Input
|
||||
id="search_fraction"
|
||||
bind:value={searchFraction}
|
||||
placeholder="Buscar por fracción, descripción, NICO..."
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Fracción</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">NICO</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Descripción</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">U.M.T</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tariffFractions as fraction (fraction.code)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
onclick={() => selectFraction(fraction)}
|
||||
>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.fraction}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.nico}</td>
|
||||
<td class="px-4 py-2">{fraction.description}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.umt}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground">
|
||||
{#if isLoadingFractions}
|
||||
Cargando fracciones...
|
||||
{:else}
|
||||
No hay fracciones disponibles
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (showFractionDialog = false)}>Cerrar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<TariffFractionSelector bind:open={showFractionDialog} onSelect={selectFraction} />
|
||||
|
||||
<!-- Dialog: Buscar fracción americana -->
|
||||
<Dialog.Root bind:open={showUSFractionDialog}>
|
||||
<Dialog.Content class="max-w-4xl w-full">
|
||||
<Dialog.Content class="w-full max-w-4xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>CATALOGO DE FRACCIONES AMERICANAS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -950,9 +859,9 @@
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
|
||||
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Código</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Prefijo</th>
|
||||
@@ -964,7 +873,7 @@
|
||||
<tbody>
|
||||
{#each usTariffFractions as fraction (fraction.id)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => selectUSFraction(fraction)}
|
||||
>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.code}</td>
|
||||
@@ -996,7 +905,7 @@
|
||||
|
||||
<!-- Dialog: Buscar depreciación -->
|
||||
<Dialog.Root bind:open={showDepreciationDialog}>
|
||||
<Dialog.Content class="max-w-4xl w-full">
|
||||
<Dialog.Content class="w-full max-w-4xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>CATALOGO DE DEPRECIACION</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -1010,9 +919,9 @@
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
|
||||
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold">Fracción</th>
|
||||
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
|
||||
@@ -1022,7 +931,7 @@
|
||||
<tbody>
|
||||
{#each depreciationCatalog as item (item.id)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => selectDepreciation(item)}
|
||||
>
|
||||
<td class="px-4 py-2">{item.fraction}</td>
|
||||
@@ -1052,7 +961,7 @@
|
||||
|
||||
<!-- Dialog: Buscar FDA -->
|
||||
<Dialog.Root bind:open={showFDADialog}>
|
||||
<Dialog.Content class="max-w-4xl w-full">
|
||||
<Dialog.Content class="w-full max-w-4xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>CATALOGO FDA</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -1066,9 +975,9 @@
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
|
||||
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold">Clave FDA</th>
|
||||
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
|
||||
@@ -1077,7 +986,7 @@
|
||||
<tbody>
|
||||
{#each fdaCatalog as item (item.id)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => selectFDA(item)}
|
||||
>
|
||||
<td class="px-4 py-2">{item.fda_key}</td>
|
||||
@@ -1106,7 +1015,7 @@
|
||||
|
||||
<!-- Dialog: Buscar Carta Porte -->
|
||||
<Dialog.Root bind:open={showCartaPorteDialog}>
|
||||
<Dialog.Content class="max-w-4xl w-full">
|
||||
<Dialog.Content class="w-full max-w-4xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>CATALOGO DE CARTA PORTE</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -1120,9 +1029,9 @@
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
|
||||
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
|
||||
<table class="w-full">
|
||||
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold">Código</th>
|
||||
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
|
||||
@@ -1131,7 +1040,7 @@
|
||||
<tbody>
|
||||
{#each cartaPorteCatalog as item (item.id)}
|
||||
<tr
|
||||
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => {
|
||||
formData.carta_porte_code = item.code;
|
||||
showCartaPorteDialog = false;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import {
|
||||
getTariffFractions,
|
||||
type TariffFraction
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: { open: boolean; onSelect: (fraction: TariffFraction) => void } = $props();
|
||||
|
||||
let tariffFractions = $state<TariffFraction[]>([]);
|
||||
let searchFraction = $state('');
|
||||
let currentPage = $state(1);
|
||||
let totalFractions = $state(0);
|
||||
let hasMoreFractions = $state(true);
|
||||
let isLoadingFractions = $state(false);
|
||||
let searchTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
// Reiniciar estado al abrir
|
||||
if (tariffFractions.length === 0) {
|
||||
searchFraction = '';
|
||||
currentPage = 1;
|
||||
totalFractions = 0;
|
||||
hasMoreFractions = true;
|
||||
loadFractions('', 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFractions(search: string, page: number = currentPage) {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId || isLoadingFractions) return;
|
||||
|
||||
isLoadingFractions = true;
|
||||
try {
|
||||
const filters = search ? { search } : {};
|
||||
const pageSize = 100;
|
||||
|
||||
const response = await getTariffFractions(page, pageSize, companyId, filters);
|
||||
|
||||
if (response.data) {
|
||||
if (page === 1) {
|
||||
tariffFractions = [...response.data.items];
|
||||
} else {
|
||||
tariffFractions = [...tariffFractions, ...response.data.items];
|
||||
}
|
||||
|
||||
totalFractions = response.data.total;
|
||||
currentPage = page;
|
||||
hasMoreFractions = tariffFractions.length < response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cargando fracciones arancelarias:', error);
|
||||
} finally {
|
||||
isLoadingFractions = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="w-full max-w-[95vw] sm:max-w-7xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>CATALOGO DE FRACCIONES SITAR - SCAII</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="search_fraction">Buscando:</Label>
|
||||
<div class="relative mt-1">
|
||||
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search_fraction"
|
||||
bind:value={searchFraction}
|
||||
placeholder="Buscar por fracción, descripción, NICO..."
|
||||
class="pl-8"
|
||||
oninput={() => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
tariffFractions = [];
|
||||
currentPage = 1;
|
||||
totalFractions = 0;
|
||||
hasMoreFractions = true;
|
||||
loadFractions(searchFraction, 1);
|
||||
}, 500);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border"
|
||||
onscroll={(e) => {
|
||||
const target = e.currentTarget;
|
||||
if (
|
||||
target.scrollHeight - target.scrollTop <= target.clientHeight + 50 &&
|
||||
hasMoreFractions &&
|
||||
!isLoadingFractions
|
||||
) {
|
||||
loadFractions(searchFraction, currentPage + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<table class="w-full">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Clave</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Fracción</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">NICO</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Descripción</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">U.M.T</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Adv. Impo</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Adv. Expo</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">DOF</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Aplica IEPS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tariffFractions as fraction (fraction.id)}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => {
|
||||
onSelect(fraction);
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<td class="px-4 py-2 font-mono whitespace-nowrap">{fraction.um_code}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.fraction}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.nico}</td>
|
||||
<td class="px-4 py-2">{fraction.description}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.umt}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.adv_impo}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.adv_expo}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.dof}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.aplica_ieps}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="9" class="px-4 py-8 text-center text-muted-foreground">
|
||||
{#if isLoadingFractions}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
Cargando fracciones...
|
||||
</div>
|
||||
{:else}
|
||||
No hay fracciones disponibles
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{#if isLoadingFractions && tariffFractions.length > 0}
|
||||
<div class="flex justify-center py-2">
|
||||
<Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,156 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import * as Table from "$lib/components/ui/table";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { Search, Loader2, Hash } from "lucide-svelte";
|
||||
import { getTariffFractions, type TariffFraction } from "$lib/api/dashboard/a76/general_catalogs/tariff-fractions";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
|
||||
import { type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
|
||||
// --- PROPS ---
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean,
|
||||
onSelect: (item: TariffFraction) => void
|
||||
} = $props();
|
||||
|
||||
// --- ESTADO ---
|
||||
let items = $state<TariffFraction[]>([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state("");
|
||||
let loaded = $state(false);
|
||||
|
||||
// Filtro local
|
||||
let filteredItems = $derived(
|
||||
items.filter(i =>
|
||||
(i.fraction || "").includes(searchTerm) ||
|
||||
(i.description || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(i.nico || "").includes(searchTerm) ||
|
||||
(i.code || "").toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open && !loaded && companyStore.activeCompany?.id) {
|
||||
loadFractions();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFractions() {
|
||||
if (!companyStore.activeCompany?.id) {
|
||||
toast.error("No hay empresa seleccionada");
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const response = await getTariffFractions(1, 1000, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
console.error("Error al cargar fracciones:", response.error);
|
||||
toast.error(`Error: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
items = response.data.items;
|
||||
loaded = true;
|
||||
} else {
|
||||
console.warn("No se encontraron fracciones:", response);
|
||||
toast.info("No se encontraron fracciones registradas");
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Excepción cargando fracciones:", e);
|
||||
toast.error(`Error de conexión: ${e.message || e}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item: TariffFraction) {
|
||||
if (onSelect) onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (item: TariffFraction) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={open}>
|
||||
<Dialog.Content class="sm:max-w-[900px] max-h-[80vh] flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Fracción Arancelaria</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Seleccione la fracción arancelaria del catálogo.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="relative w-full my-2">
|
||||
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Buscar por fracción, NICO o descripción..."
|
||||
class="pl-9"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto border rounded-md min-h-[300px]">
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-primary" />
|
||||
<p>Cargando catálogo...</p>
|
||||
</div>
|
||||
{:else if filteredItems.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
|
||||
<p>No se encontraron fracciones.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[100px]">Código</Table.Head>
|
||||
<Table.Head class="w-[120px]">Fracción</Table.Head>
|
||||
<Table.Head class="w-[80px]">NICO</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each filteredItems as item}
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-accent/50 transition-colors"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<Table.Cell class="font-mono text-xs text-muted-foreground">
|
||||
{item.code}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Hash class="h-3 w-3 text-orange-500" />
|
||||
<span class="font-mono font-bold text-orange-600 dark:text-orange-400">
|
||||
{item.fraction}
|
||||
</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-mono text-sm">
|
||||
{item.nico || '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium text-sm">
|
||||
{item.description || '-'}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<div class="text-xs text-muted-foreground self-center mr-auto">
|
||||
{filteredItems.length} registros encontrados
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<TariffFractionSelector bind:open {onSelect} />
|
||||
|
||||
@@ -1,204 +1,14 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
|
||||
import { type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (fraction: any) => void;
|
||||
onSelect: (item: TariffFraction) => void;
|
||||
} = $props();
|
||||
|
||||
let fractions: any[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let loadingMore = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
// Pagination state
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(1);
|
||||
let hasMore = $state(true);
|
||||
const pageSize = 100;
|
||||
|
||||
// Scroll container reference
|
||||
let scrollContainer: HTMLDivElement | null = $state(null);
|
||||
|
||||
async function loadFractions(page: number = 1, append: boolean = false) {
|
||||
if (page === 1) {
|
||||
loading = true;
|
||||
} else {
|
||||
loadingMore = true;
|
||||
}
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...(searchTerm.trim() && { search: searchTerm.trim() })
|
||||
});
|
||||
|
||||
const response = await fetch(`/api-sveltekit/tariff-fractions?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.items && Array.isArray(data.items)) {
|
||||
if (append) {
|
||||
fractions = [...fractions, ...data.items];
|
||||
} else {
|
||||
fractions = data.items;
|
||||
}
|
||||
currentPage = data.page;
|
||||
totalPages = data.pages;
|
||||
hasMore = currentPage < totalPages;
|
||||
} else {
|
||||
console.error('Unexpected data format:', data);
|
||||
if (!append) fractions = [];
|
||||
}
|
||||
} else {
|
||||
error = `Error: ${response.status} - ${response.statusText}`;
|
||||
console.error('Error response:', await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading tariff fractions';
|
||||
console.error('Error loading tariff fractions:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
loadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
if (!scrollContainer || loading || loadingMore || !hasMore) return;
|
||||
|
||||
const target = e.target as HTMLDivElement;
|
||||
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||
|
||||
// Load more when within 200px of bottom
|
||||
if (scrollBottom < 200) {
|
||||
loadFractions(currentPage + 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(fraction: any) {
|
||||
onSelect(fraction);
|
||||
open = false;
|
||||
}
|
||||
|
||||
// Effect to load initial data when dialog opens
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
fractions = [];
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
loadFractions(1, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to reload when search changes
|
||||
let searchDebounce: ReturnType<typeof setTimeout>;
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
clearTimeout(searchDebounce);
|
||||
searchDebounce = setTimeout(() => {
|
||||
fractions = [];
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
loadFractions(1, false);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[80vh] flex flex-col px-6">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">FRACCIONES ARANCELARIAS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-500 mt-2">
|
||||
Mostrando {fractions.length} de {currentPage * pageSize} resultados
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div bind:this={scrollContainer} onscroll={handleScroll} class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-20 text-red-600">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Código</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Fracción</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">NICO</th>
|
||||
<th class="px-3 py-2 text-left font-semibold">UMT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each fractions as fraction, i}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(fraction)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{fraction.code || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{fraction.fraction || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{fraction.description || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{fraction.nico || ''}</td>
|
||||
<td class="px-3 py-2 text-center">{fraction.umt || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if fractions.length === 0 && !loading}
|
||||
<tr>
|
||||
<td colspan="5" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#if loadingMore}
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<Loader2 class="w-6 h-6 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
<span class="ml-2 text-sm text-zinc-600 dark:text-zinc-400">Cargando más...</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !hasMore && fractions.length > 0}
|
||||
<div class="text-center py-4 text-sm text-zinc-500">Todos los resultados cargados</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-3 border-t bg-zinc-50 dark:bg-zinc-900 flex items-center justify-end gap-2">
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<TariffFractionSelector bind:open {onSelect} />
|
||||
|
||||
@@ -76,11 +76,12 @@ class CompanyStore {
|
||||
|
||||
this._loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/v1/a76/company/my-companies', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const newCompanies = await response.json();
|
||||
// Importamos dinámicamente para evitar dependencias circulares si las hubiera
|
||||
const { api } = await import('$lib/api');
|
||||
const response = await api.get<Company[]>('/v1/a76/company/my-companies');
|
||||
|
||||
if (response.data) {
|
||||
const newCompanies = response.data;
|
||||
|
||||
// Detectar si el tenant ha cambiado
|
||||
if (newCompanies.length > 0) {
|
||||
@@ -88,7 +89,6 @@ class CompanyStore {
|
||||
|
||||
// Si el tenant cambió, limpiar el store primero
|
||||
if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) {
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class CompanyStore {
|
||||
this.setActiveCompany(this._companies[0], true); // silent=true para inicialización
|
||||
}
|
||||
} else {
|
||||
console.error('Error loading companies:', response.statusText);
|
||||
console.error('Error loading companies:', response.error);
|
||||
// Si falla la carga (ej: 401), limpiar el store
|
||||
if (response.status === 401) {
|
||||
this.clear();
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
import { obtenerAtajosListaFracciones } from '$lib/config/shortcuts/dashboard/general_catalogs/tariff_fractions/list';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Estado
|
||||
let tariffFractions = $state<TariffFraction[]>([]);
|
||||
let filteredFractions = $state<TariffFraction[]>([]);
|
||||
let searchQuery = $state('');
|
||||
let isLoading = $state(false);
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(1);
|
||||
let totalRecords = $state(0);
|
||||
const pageSize = 50;
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
@@ -35,24 +36,24 @@
|
||||
loadTariffFractions();
|
||||
});
|
||||
|
||||
// Filtrar fracciones cuando cambia la búsqueda
|
||||
// Efecto para búsqueda con debounce
|
||||
$effect(() => {
|
||||
if (searchQuery.trim() === '') {
|
||||
filteredFractions = tariffFractions;
|
||||
} else {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filteredFractions = tariffFractions.filter(
|
||||
(fraction) =>
|
||||
fraction.code.toLowerCase().includes(query) ||
|
||||
fraction.fraction.toLowerCase().includes(query) ||
|
||||
(fraction.description ?? '').toLowerCase().includes(query) ||
|
||||
(fraction.nico ?? '').toLowerCase().includes(query) ||
|
||||
(fraction.umt ?? '').toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
// Limpiar timeout anterior
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
// Setup nuevo timeout
|
||||
searchTimeout = setTimeout(() => {
|
||||
// Resetear a página 1 cuando cambia la búsqueda
|
||||
if (currentPage !== 1) {
|
||||
currentPage = 1;
|
||||
}
|
||||
loadTariffFractions(1, searchQuery);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(searchTimeout);
|
||||
});
|
||||
|
||||
async function loadTariffFractions(page: number = 1) {
|
||||
async function loadTariffFractions(page: number = 1, search: string = '') {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
console.error('No hay compañía activa');
|
||||
@@ -61,10 +62,15 @@
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
const response = await getTariffFractions(page, pageSize, companyId);
|
||||
// Preparar filtros
|
||||
const filters: Record<string, any> = {};
|
||||
if (search.trim()) {
|
||||
filters.search = search.trim();
|
||||
}
|
||||
|
||||
const response = await getTariffFractions(page, pageSize, companyId, filters);
|
||||
if (response.data) {
|
||||
tariffFractions = response.data.items;
|
||||
filteredFractions = response.data.items;
|
||||
totalPages = response.data.pages;
|
||||
totalRecords = response.data.total;
|
||||
currentPage = response.data.page;
|
||||
@@ -78,31 +84,31 @@
|
||||
|
||||
async function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages && page !== currentPage) {
|
||||
await loadTariffFractions(page);
|
||||
await loadTariffFractions(page, searchQuery);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
<Card.Root>
|
||||
<Card.Header class="text-center border-b">
|
||||
<Card.Header class="border-b text-center">
|
||||
<Card.Title class="text-2xl font-bold uppercase">
|
||||
Catálogo de Fracciones SITAR - SCAII
|
||||
</Card.Title>
|
||||
<p class="text-muted-foreground mt-2">Nomenclatura arancelaria mexicana completa</p>
|
||||
<p class="mt-2 text-muted-foreground">Nomenclatura arancelaria mexicana completa</p>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="pt-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Barra de búsqueda y acciones -->
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex-1 relative">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex-1">
|
||||
<Search
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
||||
class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
|
||||
placeholder="Buscar por código, fracción, descripción, NICO o UMT (Búsqueda en servidor)..."
|
||||
class="pl-9"
|
||||
/>
|
||||
</div>
|
||||
@@ -120,13 +126,13 @@
|
||||
<span>Cargando...</span>
|
||||
</div>
|
||||
{:else}
|
||||
Mostrando {filteredFractions.length} de {totalRecords} fracciones arancelarias
|
||||
Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias
|
||||
{#if searchQuery}
|
||||
(filtrado)
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if !searchQuery && totalPages > 1}
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Página {currentPage} de {totalPages}</span>
|
||||
</div>
|
||||
@@ -134,9 +140,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabla de fracciones -->
|
||||
<div class="border rounded-md overflow-auto max-h-[600px]">
|
||||
<div class="max-h-[600px] overflow-auto rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-background z-10">
|
||||
<Table.Header class="z-10 bg-background">
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[100px]">Código</Table.Head>
|
||||
<Table.Head class="w-[120px]">Fracción</Table.Head>
|
||||
@@ -148,9 +154,9 @@
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if filteredFractions.length === 0}
|
||||
{#if tariffFractions.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="text-center py-8 text-muted-foreground">
|
||||
<Table.Cell colspan={7} class="py-8 text-center text-muted-foreground">
|
||||
{#if isLoading}
|
||||
Cargando fracciones arancelarias...
|
||||
{:else if searchQuery}
|
||||
@@ -161,7 +167,7 @@
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each filteredFractions as fraction (fraction.id)}
|
||||
{#each tariffFractions as fraction (fraction.id)}
|
||||
<Table.Row class="hover:bg-muted/50">
|
||||
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
|
||||
<Table.Cell class="font-mono text-sm font-medium"
|
||||
@@ -182,7 +188,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
{#if !searchQuery && totalPages > 1}
|
||||
{#if totalPages > 1}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
Reference in New Issue
Block a user