Merge branch 'feature/doda-endpoints-faltantes' into feature/visible-clicks-csv
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"""add action column to doda_alta_log
|
||||
|
||||
Revision ID: c3d4e5f6a7b8
|
||||
Revises: b2c3d4e5f6a7
|
||||
Create Date: 2026-04-28 13:20:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c3d4e5f6a7b8"
|
||||
down_revision = "b2c3d4e5f6a7"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"doda_alta_log",
|
||||
sa.Column("action", sa.String(length=20), nullable=True),
|
||||
schema="a76",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("doda_alta_log", "action", schema="a76")
|
||||
@@ -8,6 +8,7 @@ from pydantic import BaseModel, Field
|
||||
class DodaAltaLogCreateDTO(BaseModel):
|
||||
doda_id: Optional[int] = None
|
||||
variant: Optional[str] = Field(None, max_length=10)
|
||||
action: Optional[str] = Field(None, max_length=20)
|
||||
responsible: Optional[str] = Field(None, max_length=20)
|
||||
patent: Optional[str] = Field(None, max_length=10)
|
||||
dispatch_customs: Optional[str] = Field(None, max_length=10)
|
||||
@@ -29,6 +30,7 @@ class DodaAltaLogResponseDTO(BaseModel):
|
||||
id: int
|
||||
doda_id: Optional[int] = None
|
||||
variant: Optional[str] = None
|
||||
action: Optional[str] = None
|
||||
responsible: Optional[str] = None
|
||||
patent: Optional[str] = None
|
||||
dispatch_customs: Optional[str] = None
|
||||
|
||||
@@ -23,6 +23,7 @@ class DodaAltaLog(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# Tipo de alta (doda / pita)
|
||||
variant: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
||||
action: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
|
||||
# Datos copiados del DODA al momento del envío (para historial)
|
||||
responsible: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
|
||||
@@ -115,6 +115,7 @@ class DodaAltaLogService:
|
||||
tenant_id: int,
|
||||
variant: str,
|
||||
ext_result: dict,
|
||||
action: str = "alta",
|
||||
) -> DodaAltaLog:
|
||||
"""
|
||||
Crea un registro de log a partir de la respuesta del servicio externo de alta.
|
||||
@@ -127,6 +128,7 @@ class DodaAltaLogService:
|
||||
dto = DodaAltaLogCreateDTO(
|
||||
doda_id=doda.id,
|
||||
variant=variant,
|
||||
action=action,
|
||||
responsible=doda.responsible,
|
||||
patent=doda.patent,
|
||||
dispatch_customs=doda.dispatch_customs,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -16,6 +17,7 @@ from core.storage_s3 import get_object_bytes, object_exists
|
||||
from api.v1.modules.a76.customs_brokers import models as cb_models
|
||||
|
||||
from .models import Doda, DodaContainer, DodaAmericanPedimento, DodaPedimento
|
||||
from .alta_log_models import DodaAltaLog
|
||||
from .payload_normalizer import (
|
||||
normalize_aduana_despacho,
|
||||
normalize_aduana_seccion,
|
||||
@@ -487,6 +489,40 @@ class DodaAltaService:
|
||||
# Build full payload
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _latest_alta_log(
|
||||
self,
|
||||
doda_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
variant: str,
|
||||
) -> Optional[DodaAltaLog]:
|
||||
return (
|
||||
self.db.query(DodaAltaLog)
|
||||
.filter(
|
||||
DodaAltaLog.doda_id == doda_id,
|
||||
DodaAltaLog.tenant_id == tenant_id,
|
||||
DodaAltaLog.company_id == company_id,
|
||||
DodaAltaLog.variant == variant,
|
||||
DodaAltaLog.deleted_at.is_(None),
|
||||
)
|
||||
.order_by(DodaAltaLog.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_numero_transaccion(log_record: DodaAltaLog) -> str:
|
||||
raw_json = (log_record.result_json or "").strip()
|
||||
if raw_json:
|
||||
try:
|
||||
parsed = json.loads(raw_json)
|
||||
for key in ("numero_transaccion", "transaction_number"):
|
||||
value = parsed.get(key)
|
||||
if value:
|
||||
return str(value).strip()
|
||||
except Exception:
|
||||
logger.warning("No se pudo parsear result_json de DodaAltaLog id=%s", log_record.id)
|
||||
return ""
|
||||
|
||||
def build_alta_payload(
|
||||
self,
|
||||
doda_id: int,
|
||||
@@ -549,3 +585,59 @@ class DodaAltaService:
|
||||
}
|
||||
|
||||
return payload
|
||||
|
||||
def build_consulta_payload(
|
||||
self,
|
||||
doda_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
variant: str = "doda",
|
||||
user_email: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
payload = self.build_alta_payload(
|
||||
doda_id=doda_id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
variant=variant,
|
||||
user_email=user_email,
|
||||
)
|
||||
latest_log = self._latest_alta_log(doda_id, tenant_id, company_id, variant)
|
||||
if not latest_log:
|
||||
raise ValueError(
|
||||
"No existe un alta DODA previa para construir la consulta (falta task/log)."
|
||||
)
|
||||
numero_transaccion = self._extract_numero_transaccion(latest_log)
|
||||
if not numero_transaccion:
|
||||
raise ValueError(
|
||||
"No se encontro numero_transaccion en el ultimo resultado de alta DODA."
|
||||
)
|
||||
payload["numero_transaccion"] = numero_transaccion
|
||||
return payload
|
||||
|
||||
def build_eliminar_payload(
|
||||
self,
|
||||
doda_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
variant: str = "doda",
|
||||
user_email: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
payload = self.build_alta_payload(
|
||||
doda_id=doda_id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
variant=variant,
|
||||
user_email=user_email,
|
||||
)
|
||||
latest_log = self._latest_alta_log(doda_id, tenant_id, company_id, variant)
|
||||
if not latest_log:
|
||||
raise ValueError(
|
||||
"No existe un alta DODA previa para construir la eliminacion."
|
||||
)
|
||||
numero_integracion = (latest_log.integration_number or "").strip()
|
||||
if not numero_integracion:
|
||||
raise ValueError(
|
||||
"No se encontro numero_integracion en el historial de alta DODA."
|
||||
)
|
||||
payload["numero_integracion"] = numero_integracion
|
||||
return payload
|
||||
|
||||
@@ -17,6 +17,10 @@ class DodaExternalService:
|
||||
Endpoints:
|
||||
POST {base_url}/api/v1/doda/alta
|
||||
GET {base_url}/api/v1/doda/alta-status/{task_id}
|
||||
POST {base_url}/api/v1/doda/consulta
|
||||
GET {base_url}/api/v1/doda/consulta-status/{task_id}
|
||||
POST {base_url}/api/v1/doda/eliminar
|
||||
GET {base_url}/api/v1/doda/eliminar-status/{task_id}
|
||||
|
||||
Usa COVE_API_URL como URL base (la misma variable que COVE y Expediente).
|
||||
"""
|
||||
@@ -61,3 +65,43 @@ class DodaExternalService:
|
||||
response = client.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def post_consulta(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Envia consulta DODA y retorna {task_id, status, message}."""
|
||||
url = f"{self.base_url.rstrip('/')}/api/v1/doda/consulta"
|
||||
with httpx.Client(
|
||||
timeout=httpx.Timeout(60.0, connect=10.0), verify=self.verify_ssl
|
||||
) as client:
|
||||
response = client.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_consulta_status(self, task_id: str) -> Dict[str, Any]:
|
||||
"""Consulta el estado de una tarea de consulta DODA."""
|
||||
url = f"{self.base_url.rstrip('/')}/api/v1/doda/consulta-status/{task_id}"
|
||||
with httpx.Client(
|
||||
timeout=httpx.Timeout(30.0, connect=10.0), verify=self.verify_ssl
|
||||
) as client:
|
||||
response = client.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def post_eliminar(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Envia eliminacion DODA y retorna {task_id, status, message}."""
|
||||
url = f"{self.base_url.rstrip('/')}/api/v1/doda/eliminar"
|
||||
with httpx.Client(
|
||||
timeout=httpx.Timeout(60.0, connect=10.0), verify=self.verify_ssl
|
||||
) as client:
|
||||
response = client.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_eliminar_status(self, task_id: str) -> Dict[str, Any]:
|
||||
"""Consulta el estado de una tarea de eliminacion DODA."""
|
||||
url = f"{self.base_url.rstrip('/')}/api/v1/doda/eliminar-status/{task_id}"
|
||||
with httpx.Client(
|
||||
timeout=httpx.Timeout(30.0, connect=10.0), verify=self.verify_ssl
|
||||
) as client:
|
||||
response = client.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -60,6 +60,47 @@ from core.security import get_current_user, validate_access_to_resource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _coalesce_external_result_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
result = payload.get("result")
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
return payload
|
||||
|
||||
|
||||
def _apply_consulta_success_to_doda(
|
||||
doda: Doda,
|
||||
payload: Dict[str, Any],
|
||||
) -> None:
|
||||
source = _coalesce_external_result_payload(payload)
|
||||
mapping = {
|
||||
"integration_number": "integration_number",
|
||||
"numero_integracion": "integration_number",
|
||||
"transaction_number": "transaction_number",
|
||||
"numero_transaccion": "transaction_number",
|
||||
"sat_digital_seal": "sat_digital_seal",
|
||||
"sello_digital_sat": "sat_digital_seal",
|
||||
"sat_certificate": "sat_certificate",
|
||||
"certificado_sat": "sat_certificate",
|
||||
"serial_number": "serial_number",
|
||||
"numero_serie": "serial_number",
|
||||
"electronic_signature": "electronic_signature",
|
||||
"firma_electronica": "electronic_signature",
|
||||
"original_chain": "original_chain",
|
||||
"cadena_original": "original_chain",
|
||||
"sat_original_chain": "sat_original_chain",
|
||||
"cadena_original_sat": "sat_original_chain",
|
||||
"linq_sat_qr": "linq_sat_qr",
|
||||
"link_sat_qr": "linq_sat_qr",
|
||||
"xml_doda_sent_path": "xml_doda_sent_path",
|
||||
"xml_doda_response_path": "xml_doda_response_path",
|
||||
"status": "status",
|
||||
}
|
||||
for src_key, dst_attr in mapping.items():
|
||||
value = source.get(src_key)
|
||||
if value is not None and value != "":
|
||||
setattr(doda, dst_attr, str(value))
|
||||
|
||||
# Router independiente para rutas literales (deben registrarse antes que /{id})
|
||||
router = APIRouter(prefix="/doda", tags=["doda"])
|
||||
|
||||
@@ -638,6 +679,7 @@ async def post_doda_alta(
|
||||
tenant_id=int(tenant_id),
|
||||
variant=variant,
|
||||
ext_result=result,
|
||||
action="alta",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error persistiendo DodaAltaLog para doda_id=%s", doda_id)
|
||||
@@ -673,6 +715,269 @@ async def get_doda_alta_status(
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{doda_id}/consulta",
|
||||
summary="Enviar Consulta DODA al servicio externo (asíncrono)",
|
||||
tags=["doda-alta"],
|
||||
)
|
||||
async def post_doda_consulta(
|
||||
doda_id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
variant: str = Query("doda", description="Tipo de alta: 'doda' o 'pita'"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
user_email = (
|
||||
current_user.get("email")
|
||||
or current_user.get("preferred_username")
|
||||
or ""
|
||||
)
|
||||
service = DodaAltaService(db)
|
||||
try:
|
||||
payload = service.build_consulta_payload(
|
||||
doda_id=doda_id,
|
||||
tenant_id=int(tenant_id),
|
||||
company_id=company_id,
|
||||
variant=variant,
|
||||
user_email=user_email,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
ext_result: Dict[str, Any]
|
||||
try:
|
||||
ext = DodaExternalService()
|
||||
ext_result = ext.post_consulta(payload)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("Error al enviar consulta DODA al servicio externo: doda_id=%s", doda_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Error al contactar el servicio DODA externo (consulta): {exc}",
|
||||
) from exc
|
||||
|
||||
doda_record = DodaService.get_by_id(db, doda_id, int(tenant_id), company_id)
|
||||
if doda_record:
|
||||
try:
|
||||
DodaAltaLogService.create_from_alta_result(
|
||||
db=db,
|
||||
doda=doda_record,
|
||||
company_id=company_id,
|
||||
tenant_id=int(tenant_id),
|
||||
variant=variant,
|
||||
ext_result=ext_result,
|
||||
action="consulta",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error persistiendo DodaAltaLog(consulta) para doda_id=%s", doda_id)
|
||||
return ext_result
|
||||
|
||||
|
||||
@router.get(
|
||||
"/consulta-status/{task_id}",
|
||||
summary="Consultar estado de tarea de Consulta DODA",
|
||||
tags=["doda-alta"],
|
||||
)
|
||||
async def get_doda_consulta_status(
|
||||
task_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> Any:
|
||||
try:
|
||||
ext = DodaExternalService()
|
||||
return ext.get_consulta_status(task_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("Error consultando consulta-status DODA task_id=%s", task_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Error al consultar el estado de la consulta DODA: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{doda_id}/consulta-apply/{task_id}",
|
||||
summary="Aplicar resultado exitoso de consulta DODA al registro local",
|
||||
tags=["doda-alta"],
|
||||
)
|
||||
async def post_doda_consulta_apply(
|
||||
doda_id: int,
|
||||
task_id: str,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
tenant_id = int(validate_access_to_resource(db, company_id, current_user))
|
||||
doda_record = DodaService.get_by_id(db, doda_id, tenant_id, company_id)
|
||||
if not doda_record:
|
||||
raise HTTPException(status_code=404, detail="DODA no encontrado.")
|
||||
|
||||
try:
|
||||
ext = DodaExternalService()
|
||||
status_payload = ext.get_consulta_status(task_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("Error al consultar consulta-status para aplicar: task_id=%s", task_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Error al consultar el estado de la consulta DODA: {exc}",
|
||||
) from exc
|
||||
|
||||
task_state = str(status_payload.get("state") or status_payload.get("status") or "").upper()
|
||||
if task_state != "SUCCESS":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="La tarea de consulta aún no está en estado SUCCESS.",
|
||||
)
|
||||
|
||||
try:
|
||||
_apply_consulta_success_to_doda(doda_record, status_payload)
|
||||
if not (doda_record.status or "").strip():
|
||||
doda_record.status = "VALIDADO"
|
||||
db.add(doda_record)
|
||||
db.commit()
|
||||
db.refresh(doda_record)
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.exception("Error aplicando consulta-status al DODA id=%s", doda_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"No se pudo aplicar el resultado de consulta al DODA: {exc}",
|
||||
) from exc
|
||||
|
||||
return {
|
||||
"message": "Resultado de consulta aplicado correctamente.",
|
||||
"doda_id": doda_id,
|
||||
"task_id": task_id,
|
||||
"state": task_state,
|
||||
}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{doda_id}/eliminar",
|
||||
summary="Enviar Eliminación DODA al servicio externo (asíncrono)",
|
||||
tags=["doda-alta"],
|
||||
)
|
||||
async def post_doda_eliminar(
|
||||
doda_id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
variant: str = Query("doda", description="Tipo de alta: 'doda' o 'pita'"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
user_email = (
|
||||
current_user.get("email")
|
||||
or current_user.get("preferred_username")
|
||||
or ""
|
||||
)
|
||||
service = DodaAltaService(db)
|
||||
try:
|
||||
payload = service.build_eliminar_payload(
|
||||
doda_id=doda_id,
|
||||
tenant_id=int(tenant_id),
|
||||
company_id=company_id,
|
||||
variant=variant,
|
||||
user_email=user_email,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
ext_result: Dict[str, Any]
|
||||
try:
|
||||
ext = DodaExternalService()
|
||||
ext_result = ext.post_eliminar(payload)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("Error al enviar eliminación DODA al servicio externo: doda_id=%s", doda_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Error al contactar el servicio DODA externo (eliminación): {exc}",
|
||||
) from exc
|
||||
|
||||
doda_record = DodaService.get_by_id(db, doda_id, int(tenant_id), company_id)
|
||||
if doda_record:
|
||||
try:
|
||||
DodaAltaLogService.create_from_alta_result(
|
||||
db=db,
|
||||
doda=doda_record,
|
||||
company_id=company_id,
|
||||
tenant_id=int(tenant_id),
|
||||
variant=variant,
|
||||
ext_result=ext_result,
|
||||
action="eliminar",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error persistiendo DodaAltaLog(eliminar) para doda_id=%s", doda_id)
|
||||
|
||||
try:
|
||||
doda_record.integration_number = None
|
||||
doda_record.transaction_number = None
|
||||
doda_record.status = "PENDIENTE"
|
||||
doda_record.sat_digital_seal = None
|
||||
doda_record.sat_certificate = None
|
||||
doda_record.serial_number = None
|
||||
doda_record.electronic_signature = None
|
||||
doda_record.original_chain = None
|
||||
doda_record.sat_original_chain = None
|
||||
doda_record.linq_sat_qr = None
|
||||
doda_record.xml_doda_sent_path = None
|
||||
doda_record.xml_doda_response_path = None
|
||||
db.add(doda_record)
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.exception("Error desprocesando DODA local tras eliminación id=%s", doda_id)
|
||||
return ext_result
|
||||
|
||||
|
||||
@router.get(
|
||||
"/eliminar-status/{task_id}",
|
||||
summary="Consultar estado de tarea de Eliminación DODA",
|
||||
tags=["doda-alta"],
|
||||
)
|
||||
async def get_doda_eliminar_status(
|
||||
task_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> Any:
|
||||
try:
|
||||
ext = DodaExternalService()
|
||||
return ext.get_eliminar_status(task_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("Error consultando eliminar-status DODA task_id=%s", task_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Error al consultar el estado de la eliminación DODA: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
# ============ DODA ALTA LOG CRUD ============
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,19 @@ logger = logging.getLogger(__name__)
|
||||
class DodaService:
|
||||
"""Servicio para gestión de DODA"""
|
||||
|
||||
@staticmethod
|
||||
def _ensure_editable_doda(doda: Optional[Doda]) -> None:
|
||||
if not doda:
|
||||
return
|
||||
if (doda.integration_number or "").strip():
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=(
|
||||
"El DODA ya fue generado (tiene número de integración). "
|
||||
"Elimínelo primero para poder editarlo."
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _invalidate_report_after_mutation(
|
||||
db: Session,
|
||||
@@ -132,6 +145,7 @@ class DodaService:
|
||||
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_doda:
|
||||
return None
|
||||
DodaService._ensure_editable_doda(db_doda)
|
||||
|
||||
for key, value in doda_data.model_dump(exclude_unset=True).items():
|
||||
setattr(db_doda, key, value)
|
||||
@@ -195,6 +209,7 @@ class DodaService:
|
||||
doda = db.query(Doda).filter(Doda.id == doda_id).first()
|
||||
if not doda:
|
||||
return None
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
cv = (container_data.container_value or "").strip()
|
||||
if not cv:
|
||||
@@ -259,6 +274,8 @@ class DodaService:
|
||||
)
|
||||
if not db_container:
|
||||
return None
|
||||
doda = db.get(Doda, doda_id)
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
for key, value in container_data.model_dump(exclude_unset=True).items():
|
||||
setattr(db_container, key, value)
|
||||
@@ -308,6 +325,8 @@ class DodaService:
|
||||
)
|
||||
if not db_container:
|
||||
raise HTTPException(status_code=404, detail="Contenedor no encontrado.")
|
||||
doda = db.get(Doda, doda_id)
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
has_seals = bool(db_container.seals_detail)
|
||||
if not has_seals and db_container.seals:
|
||||
@@ -376,6 +395,7 @@ class DodaService:
|
||||
doda = db.query(Doda).filter(Doda.id == doda_id).first()
|
||||
if not doda:
|
||||
raise HTTPException(status_code=404, detail="DODA no encontrado.")
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
container = (
|
||||
db.query(DodaContainer)
|
||||
@@ -387,6 +407,8 @@ class DodaService:
|
||||
)
|
||||
if not container:
|
||||
raise HTTPException(status_code=404, detail="Contenedor no encontrado.")
|
||||
doda = db.get(Doda, doda_id)
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
raw_value = (seal_data.seal_value or "").strip()
|
||||
if not raw_value:
|
||||
@@ -504,6 +526,7 @@ class DodaService:
|
||||
doda = db.query(Doda).filter(Doda.id == doda_id).first()
|
||||
if not doda:
|
||||
return None
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
tipo = (pedimento_data.american_pedimento_type or "").strip()
|
||||
valor = (pedimento_data.american_pedimento_value or "").strip()
|
||||
@@ -595,8 +618,9 @@ class DodaService:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Pedimento americano no encontrado."
|
||||
)
|
||||
doda = db.get(Doda, doda_id)
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
try:
|
||||
doda = db.get(Doda, doda_id)
|
||||
db.delete(db_pedimento)
|
||||
db.commit()
|
||||
if doda:
|
||||
@@ -625,6 +649,7 @@ class DodaService:
|
||||
doda = db.query(Doda).filter(Doda.id == doda_id).first()
|
||||
if not doda:
|
||||
return None
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
|
||||
max_line = (
|
||||
db.query(DodaPedimento)
|
||||
@@ -681,8 +706,9 @@ class DodaService:
|
||||
)
|
||||
if not db_pedimento:
|
||||
raise HTTPException(status_code=404, detail="Pedimento no encontrado.")
|
||||
doda = db.get(Doda, doda_id)
|
||||
DodaService._ensure_editable_doda(doda)
|
||||
try:
|
||||
doda = db.get(Doda, doda_id)
|
||||
db.delete(db_pedimento)
|
||||
db.commit()
|
||||
if doda:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.doda.alta_service import DodaAltaService
|
||||
|
||||
|
||||
def test_build_consulta_payload_appends_numero_transaccion(monkeypatch):
|
||||
service = DodaAltaService(db=None)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"build_alta_payload",
|
||||
lambda **kwargs: {"base": "payload"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_latest_alta_log",
|
||||
lambda *args, **kwargs: SimpleNamespace(
|
||||
id=9,
|
||||
result_json=json.dumps({"numero_transaccion": "TX-001"}),
|
||||
integration_number="INT-001",
|
||||
),
|
||||
)
|
||||
|
||||
payload = service.build_consulta_payload(
|
||||
doda_id=1, tenant_id=1, company_id=1, variant="doda", user_email="u@test.com"
|
||||
)
|
||||
assert payload["base"] == "payload"
|
||||
assert payload["numero_transaccion"] == "TX-001"
|
||||
|
||||
|
||||
def test_build_consulta_payload_fails_when_numero_transaccion_missing(monkeypatch):
|
||||
service = DodaAltaService(db=None)
|
||||
monkeypatch.setattr(service, "build_alta_payload", lambda **kwargs: {})
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_latest_alta_log",
|
||||
lambda *args, **kwargs: SimpleNamespace(
|
||||
id=10, result_json=json.dumps({"other": "value"}), integration_number="INT-001"
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="numero_transaccion"):
|
||||
service.build_consulta_payload(
|
||||
doda_id=1, tenant_id=1, company_id=1, variant="doda", user_email=""
|
||||
)
|
||||
|
||||
|
||||
def test_build_eliminar_payload_appends_numero_integracion(monkeypatch):
|
||||
service = DodaAltaService(db=None)
|
||||
monkeypatch.setattr(service, "build_alta_payload", lambda **kwargs: {"base": "payload"})
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_latest_alta_log",
|
||||
lambda *args, **kwargs: SimpleNamespace(
|
||||
id=11,
|
||||
result_json="{}",
|
||||
integration_number="INT-900",
|
||||
),
|
||||
)
|
||||
|
||||
payload = service.build_eliminar_payload(
|
||||
doda_id=2, tenant_id=1, company_id=1, variant="doda", user_email="user@test.com"
|
||||
)
|
||||
assert payload["base"] == "payload"
|
||||
assert payload["numero_integracion"] == "INT-900"
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.doda.external_service import DodaExternalService
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, payload: Dict[str, Any]):
|
||||
self._payload = payload
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return self._payload
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
calls = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def post(self, url: str, json: Dict[str, Any]):
|
||||
_FakeClient.calls.append(("POST", url, json))
|
||||
return _FakeResponse({"task_id": "t-1", "status": "queued", "message": "ok"})
|
||||
|
||||
def get(self, url: str):
|
||||
_FakeClient.calls.append(("GET", url, None))
|
||||
return _FakeResponse({"task_id": "t-1", "status": "done", "message": "ok"})
|
||||
|
||||
|
||||
def test_external_service_supports_consulta_and_eliminar(monkeypatch):
|
||||
from api.v1.modules.a76.general_catalogs.doda import external_service as module
|
||||
|
||||
_FakeClient.calls = []
|
||||
monkeypatch.setattr(module, "httpx", module.httpx)
|
||||
monkeypatch.setattr(module.httpx, "Client", _FakeClient)
|
||||
|
||||
service = DodaExternalService()
|
||||
service.base_url = "http://example.test"
|
||||
|
||||
payload = {"foo": "bar"}
|
||||
consulta = service.post_consulta(payload)
|
||||
consulta_status = service.get_consulta_status("abc123")
|
||||
eliminar = service.post_eliminar(payload)
|
||||
eliminar_status = service.get_eliminar_status("abc123")
|
||||
|
||||
assert consulta["task_id"] == "t-1"
|
||||
assert consulta_status["status"] == "done"
|
||||
assert eliminar["status"] == "queued"
|
||||
assert eliminar_status["task_id"] == "t-1"
|
||||
assert _FakeClient.calls == [
|
||||
("POST", "http://example.test/api/v1/doda/consulta", payload),
|
||||
("GET", "http://example.test/api/v1/doda/consulta-status/abc123", None),
|
||||
("POST", "http://example.test/api/v1/doda/eliminar", payload),
|
||||
("GET", "http://example.test/api/v1/doda/eliminar-status/abc123", None),
|
||||
]
|
||||
@@ -478,6 +478,56 @@ export async function getDodaAltaStatus(
|
||||
return api.get<DodaAltaStatusResponse>(`/v1/a76/doda/alta-status/${taskId}`);
|
||||
}
|
||||
|
||||
export async function postDodaConsulta(
|
||||
dodaId: number,
|
||||
companyId: number,
|
||||
variant: 'doda' | 'pita' = 'doda'
|
||||
): Promise<ApiResponse<DodaAltaResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
variant,
|
||||
});
|
||||
return api.post<DodaAltaResponse>(`/v1/a76/doda/${dodaId}/consulta?${params}`, {});
|
||||
}
|
||||
|
||||
export async function getDodaConsultaStatus(
|
||||
taskId: string
|
||||
): Promise<ApiResponse<DodaAltaStatusResponse>> {
|
||||
return api.get<DodaAltaStatusResponse>(`/v1/a76/doda/consulta-status/${taskId}`);
|
||||
}
|
||||
|
||||
export async function postDodaConsultaApply(
|
||||
dodaId: number,
|
||||
taskId: string,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Record<string, unknown>>> {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
});
|
||||
return api.post<Record<string, unknown>>(
|
||||
`/v1/a76/doda/${dodaId}/consulta-apply/${taskId}?${params}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export async function postDodaEliminar(
|
||||
dodaId: number,
|
||||
companyId: number,
|
||||
variant: 'doda' | 'pita' = 'doda'
|
||||
): Promise<ApiResponse<DodaAltaResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
variant,
|
||||
});
|
||||
return api.post<DodaAltaResponse>(`/v1/a76/doda/${dodaId}/eliminar?${params}`, {});
|
||||
}
|
||||
|
||||
export async function getDodaEliminarStatus(
|
||||
taskId: string
|
||||
): Promise<ApiResponse<DodaAltaStatusResponse>> {
|
||||
return api.get<DodaAltaStatusResponse>(`/v1/a76/doda/eliminar-status/${taskId}`);
|
||||
}
|
||||
|
||||
export async function getDodaElegibilidad(
|
||||
dodaId: number,
|
||||
companyId: number,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
getDodaAltaStatus,
|
||||
type DodaAltaStatusResponse
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
@@ -14,6 +15,9 @@
|
||||
taskId,
|
||||
dodaId,
|
||||
variant = 'doda',
|
||||
title = m['sidebar.doda_alta.progress_title'](),
|
||||
description = 'Task ID',
|
||||
getStatus = getDodaAltaStatus,
|
||||
onComplete,
|
||||
onCancel
|
||||
}: {
|
||||
@@ -21,6 +25,9 @@
|
||||
taskId: string;
|
||||
dodaId?: number;
|
||||
variant?: 'doda' | 'pita';
|
||||
title?: string;
|
||||
description?: string;
|
||||
getStatus?: (taskId: string) => Promise<ApiResponse<DodaAltaStatusResponse>>;
|
||||
onComplete?: (result: DodaAltaStatusResponse) => void;
|
||||
onCancel?: () => void;
|
||||
} = $props();
|
||||
@@ -84,7 +91,7 @@
|
||||
if (!taskId || !pollingActive || pollInFlight) return;
|
||||
pollInFlight = true;
|
||||
try {
|
||||
const res = await getDodaAltaStatus(taskId);
|
||||
const res = await getStatus(taskId);
|
||||
|
||||
if (res.error) {
|
||||
consecutivePollErrors += 1;
|
||||
@@ -151,8 +158,8 @@
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{m['sidebar.doda_alta.progress_title']()}</Dialog.Title>
|
||||
<Dialog.Description>Alta {variantLabel} — Task ID: {taskId}</Dialog.Description>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>{description} {variantLabel} — Task ID: {taskId}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-4 py-2">
|
||||
|
||||
@@ -1,357 +1,538 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/doda/columns';
|
||||
import DodaFormModal from '$lib/components/dashboard/general_catalogs/doda/doda-form-modal.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 { Pencil, Plus, Trash2, RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/list';
|
||||
import { dodaApi, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
import {
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Search,
|
||||
RotateCcw,
|
||||
Send,
|
||||
Loader2,
|
||||
FileSpreadsheet,
|
||||
Table
|
||||
} from 'lucide-svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { isPitaCustomsClearance } from '$lib/components/dashboard/general_catalogs/doda/doda-form-helpers';
|
||||
|
||||
let { data } = $props();
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/doda/columns';
|
||||
import DodaProgressDialog from '$lib/components/dashboard/despacho/doda/doda-progress-dialog.svelte';
|
||||
import DodaExportExcelDialog from '$lib/components/dashboard/despacho/doda/doda-export-excel-dialog.svelte';
|
||||
import { applyOptimisticDelete } from '$lib/components/dashboard/general_catalogs/doda/delete-list-state';
|
||||
|
||||
let dialogOpen = $state(false);
|
||||
let editingItem = $state<Doda | null>(null);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
import {
|
||||
getDodas,
|
||||
deleteDoda,
|
||||
exportDodaPedimentosDetail,
|
||||
postDodaAlta,
|
||||
postDodaConsulta,
|
||||
postDodaConsultaApply,
|
||||
postDodaEliminar,
|
||||
getDodaAltaStatus,
|
||||
getDodaElegibilidad,
|
||||
getDodaConsultaStatus,
|
||||
getDodaEliminarStatus,
|
||||
type DodaAltaStatusResponse,
|
||||
type Doda
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'cat_doda.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'cat_doda.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'cat_doda.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'cat_doda.delete'));
|
||||
let { data } = $props();
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
let allDodas = $state<Doda[]>(data.dodas?.items || []);
|
||||
let dodaPage = $state(data.dodas?.page || 1);
|
||||
let dodaPageSize = $state(50);
|
||||
let dodaTotal = $state(data.dodas?.total || 0);
|
||||
let dodaLoading = $state(false);
|
||||
let dodaHasMore = $derived(allDodas.length < dodaTotal);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista DODA',
|
||||
obtenerAtajosListaDoda({
|
||||
manejarNuevo: () => {
|
||||
if (!canCreate) return;
|
||||
editingItem = null;
|
||||
dialogOpen = true;
|
||||
},
|
||||
manejarActualizar: () => reloadData()
|
||||
})
|
||||
);
|
||||
let filters = $state({
|
||||
integration_number: $page.url.searchParams.get('integration_number') || '',
|
||||
patent: $page.url.searchParams.get('patent') || '',
|
||||
status: $page.url.searchParams.get('status') || '',
|
||||
operation_type: $page.url.searchParams.get('operation_type') || ''
|
||||
});
|
||||
|
||||
// Filtros
|
||||
let filters = $state({
|
||||
integration_number: $page.url.searchParams.get('integration_number') || '',
|
||||
patent: $page.url.searchParams.get('patent') || '',
|
||||
status: $page.url.searchParams.get('status') || '',
|
||||
operation_type: $page.url.searchParams.get('operation_type') || ''
|
||||
});
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
let dodaFilterTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
let allItems = $state<Doda[]>(data.dodas?.items || []);
|
||||
let currentPage = $state(data.dodas?.page || 1);
|
||||
let pageSize = $state(data.dodas?.page_size || 50);
|
||||
let totalItems = $state(data.dodas?.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let selectedIds = $state<(string | number)[]>([]);
|
||||
const selectedItem = $derived(
|
||||
selectedIds.length === 1
|
||||
? allItems.find((item) => String(item.id) === String(selectedIds[0])) ?? null
|
||||
: null
|
||||
);
|
||||
let selectedDodaIds = $state<(string | number)[]>([]);
|
||||
const selectedDoda = $derived(
|
||||
selectedDodaIds.length === 1
|
||||
? allDodas.find((item) => String(item.id) === String(selectedDodaIds[0])) ?? null
|
||||
: null
|
||||
);
|
||||
const altaVariant = $derived<'doda' | 'pita'>(
|
||||
selectedDoda && isPitaCustomsClearance(selectedDoda.customs_clearance) ? 'pita' : 'doda'
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (data.dodas) {
|
||||
allItems = data.dodas.items || [];
|
||||
currentPage = data.dodas.page || 1;
|
||||
totalItems = data.dodas.total || 0;
|
||||
pageSize = data.dodas.page_size || pageSize;
|
||||
}
|
||||
});
|
||||
let progressDialogOpen = $state(false);
|
||||
let exportDialogOpen = $state(false);
|
||||
let currentTaskId = $state('');
|
||||
let currentVariant = $state<'doda' | 'pita'>('doda');
|
||||
let progressMode = $state<'alta' | 'consulta' | 'eliminar'>('alta');
|
||||
let altaLoading = $state(false);
|
||||
let consultaLoading = $state(false);
|
||||
let eliminarExternoLoading = $state(false);
|
||||
let deleteLoading = $state(false);
|
||||
let pedimentosExportLoading = $state(false);
|
||||
const hasIntegration = $derived(!!(selectedDoda?.integration_number || '').trim());
|
||||
|
||||
async function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await dodaApi.list(1, pageSize, companyStore.activeCompany.id, {
|
||||
integration_number: filters.integration_number || undefined,
|
||||
patent: filters.patent || undefined,
|
||||
status: filters.status || undefined,
|
||||
operation_type: filters.operation_type || undefined
|
||||
});
|
||||
const payload = response.data;
|
||||
if (payload?.items) {
|
||||
allItems = payload.items;
|
||||
currentPage = payload.page || 1;
|
||||
totalItems = payload.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error aplicando filtros';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
$effect(() => {
|
||||
if (data.dodas) {
|
||||
allDodas = data.dodas.items || [];
|
||||
dodaPage = data.dodas.page || 1;
|
||||
dodaTotal = data.dodas.total || 0;
|
||||
}
|
||||
});
|
||||
|
||||
const url = new URL($page.url);
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value) url.searchParams.set(key, value);
|
||||
else url.searchParams.delete(key);
|
||||
});
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
$effect(() => {
|
||||
const _ = { ...filters };
|
||||
clearTimeout(dodaFilterTimeout);
|
||||
dodaFilterTimeout = setTimeout(() => reloadDodas(), 400);
|
||||
});
|
||||
|
||||
function clearFilters() {
|
||||
filters.integration_number = '';
|
||||
filters.patent = '';
|
||||
filters.status = '';
|
||||
filters.operation_type = '';
|
||||
handleSearch();
|
||||
}
|
||||
async function reloadDodas() {
|
||||
if (!browser) return;
|
||||
dodaLoading = true;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
const active = Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== ''));
|
||||
const res = await getDodas(1, dodaPageSize, active, Number(companyId));
|
||||
if (res.data) {
|
||||
allDodas = res.data.items;
|
||||
dodaPage = 1;
|
||||
dodaTotal = res.data.total;
|
||||
selectedDodaIds = [];
|
||||
}
|
||||
} catch {
|
||||
if (allDodas.length > 0) toast.error('Error al recargar DODAs');
|
||||
} finally {
|
||||
dodaLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await dodaApi.list(currentPage + 1, pageSize, companyStore.activeCompany.id, {
|
||||
integration_number: filters.integration_number || undefined,
|
||||
patent: filters.patent || undefined,
|
||||
status: filters.status || undefined,
|
||||
operation_type: filters.operation_type || undefined
|
||||
});
|
||||
const payload = response.data;
|
||||
if (payload?.items) {
|
||||
allItems = [...allItems, ...payload.items];
|
||||
currentPage = payload.page || (currentPage + 1);
|
||||
totalItems = payload.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error cargando mas datos';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function loadMoreDodas() {
|
||||
if (dodaLoading || !dodaHasMore) return;
|
||||
dodaLoading = true;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
const active = Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== ''));
|
||||
const res = await getDodas(dodaPage + 1, dodaPageSize, active, Number(companyId));
|
||||
if (res.data) {
|
||||
allDodas = [...allDodas, ...res.data.items];
|
||||
dodaPage++;
|
||||
dodaTotal = res.data.total;
|
||||
}
|
||||
} finally {
|
||||
dodaLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (!companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await dodaApi.list(1, pageSize, companyStore.activeCompany.id, {
|
||||
integration_number: filters.integration_number || undefined,
|
||||
patent: filters.patent || undefined,
|
||||
status: filters.status || undefined,
|
||||
operation_type: filters.operation_type || undefined
|
||||
});
|
||||
const payload = response.data;
|
||||
if (payload?.items) {
|
||||
allItems = payload.items;
|
||||
currentPage = payload.page || 1;
|
||||
totalItems = payload.total;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error al recargar datos';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
function handleEdit() {
|
||||
if (selectedDoda) {
|
||||
void goto(`/dashboard/general_catalogs/doda?doda_id=${selectedDoda.id}`, { noScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
dialogOpen = false;
|
||||
editingItem = null;
|
||||
selectedIds = [];
|
||||
reloadData();
|
||||
}
|
||||
async function handleDelete() {
|
||||
if (deleteLoading) return;
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error(m['sidebar.doda_alta.delete_missing_company']());
|
||||
return;
|
||||
}
|
||||
if (selectedDodaIds.length !== 1) {
|
||||
toast.error(m['sidebar.doda_alta.delete_select_one']());
|
||||
return;
|
||||
}
|
||||
if (!selectedDoda) {
|
||||
toast.error(m['sidebar.doda_alta.delete_not_found']());
|
||||
return;
|
||||
}
|
||||
if (!confirm(m['sidebar.doda_alta.confirm_delete']())) return;
|
||||
deleteLoading = true;
|
||||
try {
|
||||
const deletedId = selectedDoda.id;
|
||||
await deleteDoda(deletedId, companyStore.activeCompany.id);
|
||||
toast.success(m['sidebar.doda_alta.delete_success']());
|
||||
const next = applyOptimisticDelete(allDodas, dodaTotal, deletedId);
|
||||
allDodas = next.items;
|
||||
dodaTotal = next.total;
|
||||
selectedDodaIds = [];
|
||||
await reloadDodas();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : m['sidebar.doda_alta.delete_error']();
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
deleteLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNew() {
|
||||
if (!canCreate) return;
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('doda_id', 'new');
|
||||
goto(url.toString(), { replaceState: true });
|
||||
}
|
||||
async function handleExportPedimentos() {
|
||||
if (pedimentosExportLoading) return;
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error(m['sidebar.doda_alta.delete_missing_company']());
|
||||
return;
|
||||
}
|
||||
if (selectedDodaIds.length !== 1 || !selectedDoda) {
|
||||
toast.error(m['sidebar.doda_alta.delete_select_one']());
|
||||
return;
|
||||
}
|
||||
pedimentosExportLoading = true;
|
||||
try {
|
||||
await exportDodaPedimentosDetail(selectedDoda.id, companyStore.activeCompany.id, 'xls');
|
||||
toast.success(m['sidebar.doda_alta.export_pedimentos_success']());
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : m['sidebar.doda_alta.export_pedimentos_error']();
|
||||
toast.error(msg || m['sidebar.doda_alta.export_pedimentos_error']());
|
||||
} finally {
|
||||
pedimentosExportLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
if (!selectedItem || !canEdit) return;
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('doda_id', selectedItem.id.toString());
|
||||
goto(url.toString(), { replaceState: true });
|
||||
}
|
||||
function clearFilters() {
|
||||
filters = { integration_number: '', patent: '', status: '', operation_type: '' };
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!selectedItem || !canDelete || !companyStore.activeCompany) return;
|
||||
if (confirm('¿Eliminar este registro?')) {
|
||||
try {
|
||||
await dodaApi.delete(selectedItem.id, companyStore.activeCompany.id);
|
||||
selectedIds = [];
|
||||
reloadData();
|
||||
} catch (err) {
|
||||
error = 'Error al eliminar';
|
||||
}
|
||||
}
|
||||
}
|
||||
async function handleAlta() {
|
||||
if (!selectedDoda || !companyStore.activeCompany) return;
|
||||
const companyId = companyStore.activeCompany.id;
|
||||
const dodaId = selectedDoda.id;
|
||||
const variant = altaVariant;
|
||||
altaLoading = true;
|
||||
try {
|
||||
const elig = await getDodaElegibilidad(dodaId, companyId, variant);
|
||||
if (elig.error) {
|
||||
toast.error(`Error al verificar elegibilidad: ${elig.error}`);
|
||||
return;
|
||||
}
|
||||
if (elig.data && !elig.data.can_alta) {
|
||||
const msgs = elig.data.reasons.map((r) => `• ${r.message}`).join('\n');
|
||||
toast.error(msgs || m['sidebar.doda_alta.eligibility_error']());
|
||||
return;
|
||||
}
|
||||
const resp = await postDodaAlta(dodaId, companyId, variant);
|
||||
if (resp.error) {
|
||||
toast.error(`Error al enviar alta: ${resp.error}`);
|
||||
return;
|
||||
}
|
||||
currentTaskId = resp.data!.task_id;
|
||||
currentVariant = variant;
|
||||
progressDialogOpen = true;
|
||||
} finally {
|
||||
altaLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePrint() {
|
||||
if (!selectedItem || !companyStore.activeCompany) return;
|
||||
try {
|
||||
const { printDoda } = await import('$lib/api/dashboard/a76/general_catalogs/doda');
|
||||
await printDoda(selectedItem.id, companyStore.activeCompany.id);
|
||||
} catch (err) {
|
||||
error = 'Error al imprimir PDF';
|
||||
}
|
||||
}
|
||||
async function handleConsultar() {
|
||||
if (!selectedDoda || !companyStore.activeCompany || consultaLoading) return;
|
||||
if (!hasIntegration) {
|
||||
toast.error('El DODA aún no está generado para consultar.');
|
||||
return;
|
||||
}
|
||||
consultaLoading = true;
|
||||
try {
|
||||
const resp = await postDodaConsulta(selectedDoda.id, companyStore.activeCompany.id, altaVariant);
|
||||
if (resp.error || !resp.data?.task_id) {
|
||||
toast.error(resp.error || 'Error al enviar consulta DODA');
|
||||
return;
|
||||
}
|
||||
progressMode = 'consulta';
|
||||
currentTaskId = resp.data.task_id;
|
||||
currentVariant = altaVariant;
|
||||
progressDialogOpen = true;
|
||||
} finally {
|
||||
consultaLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowClick(row: Doda) {
|
||||
selectedIds = selectedIds.includes(row.id) ? [] : [row.id];
|
||||
}
|
||||
async function handleEliminarExterno() {
|
||||
if (!selectedDoda || !companyStore.activeCompany || eliminarExternoLoading) return;
|
||||
if (!hasIntegration) {
|
||||
toast.error('El DODA aún no está generado para eliminar externamente.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('¿Deseas eliminar este DODA en el servicio externo para volver a editarlo?')) return;
|
||||
eliminarExternoLoading = true;
|
||||
try {
|
||||
const resp = await postDodaEliminar(selectedDoda.id, companyStore.activeCompany.id, altaVariant);
|
||||
if (resp.error || !resp.data?.task_id) {
|
||||
toast.error(resp.error || 'Error al enviar eliminación DODA');
|
||||
return;
|
||||
}
|
||||
progressMode = 'eliminar';
|
||||
currentTaskId = resp.data.task_id;
|
||||
currentVariant = altaVariant;
|
||||
progressDialogOpen = true;
|
||||
} finally {
|
||||
eliminarExternoLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowDoubleClick(row: Doda) {
|
||||
if (canEdit) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('doda_id', row.id.toString());
|
||||
goto(url.toString(), { replaceState: true });
|
||||
}
|
||||
}
|
||||
async function onProgressComplete(_result: DodaAltaStatusResponse) {
|
||||
progressDialogOpen = false;
|
||||
if (progressMode === 'consulta' && selectedDoda && companyStore.activeCompany) {
|
||||
const applyResp = await postDodaConsultaApply(
|
||||
selectedDoda.id,
|
||||
currentTaskId,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
if (applyResp.error) {
|
||||
toast.error(`Consulta completada, pero no se pudo aplicar al DODA: ${applyResp.error}`);
|
||||
}
|
||||
}
|
||||
await reloadDodas();
|
||||
if (progressMode === 'eliminar') {
|
||||
toast.success('Eliminación DODA completada. El registro quedó editable nuevamente.');
|
||||
} else if (progressMode === 'consulta') {
|
||||
toast.success('Consulta DODA completada y aplicada al registro.');
|
||||
} else {
|
||||
toast.success(m['sidebar.doda_alta.progress_success']());
|
||||
}
|
||||
}
|
||||
|
||||
const columns = $derived(createColumns('es', handleSuccess, { canEdit, canDelete }));
|
||||
const progressTitle = $derived(
|
||||
progressMode === 'consulta'
|
||||
? 'Consulta DODA'
|
||||
: progressMode === 'eliminar'
|
||||
? 'Eliminación DODA'
|
||||
: m['sidebar.doda_alta.progress_title']()
|
||||
);
|
||||
const progressDescription = $derived(
|
||||
progressMode === 'consulta'
|
||||
? 'Consulta'
|
||||
: progressMode === 'eliminar'
|
||||
? 'Eliminación'
|
||||
: 'Alta'
|
||||
);
|
||||
const progressStatusGetter = $derived(
|
||||
progressMode === 'consulta'
|
||||
? getDodaConsultaStatus
|
||||
: progressMode === 'eliminar'
|
||||
? getDodaEliminarStatus
|
||||
: getDodaAltaStatus
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex h-[calc(100svh-4rem)] flex-col gap-6 overflow-hidden p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]">
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
|
||||
<p class="text-muted-foreground">Catálogo de DODA</p>
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">{m['sidebar.doda_alta.title']()}</h1>
|
||||
<p class="text-muted-foreground">{m['sidebar.doda_alta.subtitle']()}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" onclick={() => reloadDodas()} disabled={dodaLoading}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {dodaLoading ? 'animate-spin' : ''}" />
|
||||
{m['sidebar.doda_alta.refresh']()}
|
||||
</Button>
|
||||
<Button size="sm" onclick={() => goto('/dashboard/general_catalogs/doda?doda_id=new')}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
{m['sidebar.doda_alta.action_new']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<Card.Title>{m['sidebar.doda_alta.table_title']()}</Card.Title>
|
||||
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[220px_180px_170px_170px_auto] xl:items-center">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={m['sidebar.doda_alta.filter_integration_number']()}
|
||||
bind:value={filters.integration_number}
|
||||
class="h-9 bg-card pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={m['sidebar.doda_alta.filter_patent']()}
|
||||
bind:value={filters.patent}
|
||||
class="h-9 bg-card"
|
||||
/>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={filters.status}
|
||||
onValueChange={(v) => (filters.status = v)}
|
||||
>
|
||||
<Select.Trigger class="h-9 w-full bg-card">
|
||||
{filters.status || m['sidebar.doda_alta.filter_status']()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">Todos</Select.Item>
|
||||
<Select.Item value="PENDIENTE">PENDIENTE</Select.Item>
|
||||
<Select.Item value="GENERADO">GENERADO</Select.Item>
|
||||
<Select.Item value="VALIDADO">VALIDADO</Select.Item>
|
||||
<Select.Item value="ELIMINADO">ELIMINADO</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={filters.operation_type}
|
||||
onValueChange={(v) => (filters.operation_type = v)}
|
||||
>
|
||||
<Select.Trigger class="h-9 w-full bg-card">
|
||||
{filters.operation_type === 'I'
|
||||
? 'Importación'
|
||||
: filters.operation_type === 'E'
|
||||
? 'Exportación'
|
||||
: m['sidebar.doda_alta.filter_operation_type']()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">Todas</Select.Item>
|
||||
<Select.Item value="I">I - Importación</Select.Item>
|
||||
<Select.Item value="E">E - Exportación</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
|
||||
<RotateCcw class="mr-2 h-4 w-4" />
|
||||
Limpiar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0">
|
||||
<div class="rounded-md border bg-background">
|
||||
<DataTable
|
||||
data={allDodas}
|
||||
columns={createColumns()}
|
||||
loading={dodaLoading}
|
||||
hasMore={dodaHasMore}
|
||||
loadMore={loadMoreDodas}
|
||||
selectedId={selectedDodaIds.length === 1 ? selectedDodaIds[0] : null}
|
||||
onRowClick={(row) => {
|
||||
selectedDodaIds = selectedDodaIds.includes(row.id) ? [] : [row.id];
|
||||
}}
|
||||
onRowDoubleClick={(item) =>
|
||||
goto(`/dashboard/general_catalogs/doda?doda_id=${item.id}`, { noScroll: true })}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground pt-2">
|
||||
Mostrando {allDodas.length} de {dodaTotal} registros
|
||||
<span class="ml-2">•</span>
|
||||
<span class="ml-2">Filtros activos: {Object.values(filters).filter((v) => v !== '').length}</span>
|
||||
</div>
|
||||
|
||||
<div class="h-20"></div>
|
||||
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-50 ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
>
|
||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleEdit}
|
||||
disabled={selectedDodaIds.length !== 1 || hasIntegration}
|
||||
>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
{m['sidebar.doda_alta.action_edit']()}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onclick={handleConsultar}
|
||||
disabled={selectedDodaIds.length !== 1 || !hasIntegration || consultaLoading}
|
||||
>
|
||||
{#if consultaLoading}
|
||||
<Loader2 size={16} class="mr-2 animate-spin" />
|
||||
{:else}
|
||||
<Search size={16} class="mr-2" />
|
||||
{/if}
|
||||
Consultar DODA
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
class="border border-amber-500/30 bg-amber-500/10 text-amber-700 hover:bg-amber-500/20"
|
||||
onclick={handleEliminarExterno}
|
||||
disabled={selectedDodaIds.length !== 1 || !hasIntegration || eliminarExternoLoading}
|
||||
>
|
||||
{#if eliminarExternoLoading}
|
||||
<Loader2 size={16} class="mr-2 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
Eliminar DODA
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleDelete}
|
||||
disabled={selectedDodaIds.length !== 1 || deleteLoading}
|
||||
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
{#if deleteLoading}
|
||||
<Loader2 size={16} class="mr-2 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
{m['sidebar.doda_alta.action_delete']()}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={() => (exportDialogOpen = true)}>
|
||||
<FileSpreadsheet size={16} class="mr-2" />
|
||||
{m['sidebar.doda_alta.action_export_excel']()}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
class="border border-primary/25 bg-primary/10 text-primary hover:bg-primary/15"
|
||||
onclick={handleExportPedimentos}
|
||||
disabled={selectedDodaIds.length !== 1 || pedimentosExportLoading || !companyStore.activeCompany}
|
||||
title={m['sidebar.doda_alta.action_export_pedimentos']()}
|
||||
>
|
||||
{#if pedimentosExportLoading}
|
||||
<Loader2 size={16} class="mr-2 animate-spin" />
|
||||
{:else}
|
||||
<Table size={16} class="mr-2" />
|
||||
{/if}
|
||||
{m['sidebar.doda_alta.action_export_pedimentos']()}
|
||||
</Button>
|
||||
<Separator orientation="vertical" class="mx-1 h-8 hidden sm:block" />
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={handleAlta}
|
||||
disabled={selectedDodaIds.length !== 1 || altaLoading || hasIntegration}
|
||||
title={altaVariant === 'pita' ? 'PITA' : 'DODA'}
|
||||
>
|
||||
{#if altaLoading}
|
||||
<Loader2 size={16} class="mr-2 animate-spin" />
|
||||
{:else}
|
||||
<Send size={16} class="mr-2" />
|
||||
{/if}
|
||||
{m['sidebar.doda_alta.action_generar']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" /> Actualizar
|
||||
</Button>
|
||||
{#if !isError && canCreate}
|
||||
<Button class="h-9" onclick={handleNew}>
|
||||
<Plus class="mr-2 h-4 w-4" /> Nuevo Registro
|
||||
</Button>
|
||||
|
||||
<DodaExportExcelDialog bind:open={exportDialogOpen} companyId={companyStore.activeCompany?.id} />
|
||||
|
||||
{#if progressDialogOpen}
|
||||
<DodaProgressDialog
|
||||
bind:open={progressDialogOpen}
|
||||
taskId={currentTaskId}
|
||||
dodaId={selectedDoda?.id}
|
||||
variant={currentVariant}
|
||||
title={progressTitle}
|
||||
description={progressDescription}
|
||||
getStatus={progressStatusGetter}
|
||||
onComplete={onProgressComplete}
|
||||
onCancel={() => (progressDialogOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: cat_doda.view' : error || ''}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<Card.Title>Listado de DODA</Card.Title>
|
||||
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[200px_140px_160px_160px_auto] xl:items-center">
|
||||
<Input placeholder="Folio" bind:value={filters.integration_number} oninput={handleSearch} class="h-9 bg-card" />
|
||||
<Input placeholder="Patente" bind:value={filters.patent} oninput={handleSearch} class="h-9 bg-card" />
|
||||
|
||||
<select
|
||||
bind:value={filters.status}
|
||||
onchange={handleSearch}
|
||||
class="h-9 w-full rounded-md border border-input bg-card px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<option value="">Estatus</option>
|
||||
<option value="PENDIENTE">PENDIENTE</option>
|
||||
<option value="GENERADO">GENERADO</option>
|
||||
<option value="VALIDADO">VALIDADO</option>
|
||||
<option value="ELIMINADO">ELIMINADO</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
bind:value={filters.operation_type}
|
||||
onchange={handleSearch}
|
||||
class="h-9 w-full rounded-md border border-input bg-card px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
<option value="">Operación</option>
|
||||
<option value="I">I - Importación</option>
|
||||
<option value="E">E - Exportación</option>
|
||||
</select>
|
||||
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
|
||||
Limpiar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 flex-1 overflow-hidden p-0">
|
||||
<div class="h-full overflow-hidden rounded-md border bg-background">
|
||||
<InfiniteDataTable
|
||||
data={allItems} {columns} {loading} {hasMore} {loadMore}
|
||||
{selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)}
|
||||
onRowClick={handleRowClick}
|
||||
onRowDoubleClick={handleRowDoubleClick}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
|
||||
|
||||
<div class="fixed right-0 bottom-0 left-0 z-50 ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80">
|
||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{#if selectedItem}
|
||||
Seleccionado: <span class="font-medium text-foreground">{selectedItem.integration_number || 'S/N'}</span>
|
||||
{:else}
|
||||
Selecciona un registro para ver acciones
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" size="sm" onclick={reloadData} disabled={loading}>
|
||||
Actualizar
|
||||
</Button>
|
||||
|
||||
{#if canEdit}
|
||||
<Button variant="outline" size="sm" onclick={handleEdit} disabled={!selectedItem}>
|
||||
<Pencil size={16} class="mr-2" /> Editar
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if canDelete}
|
||||
<Button variant="outline" size="sm" onclick={handleDelete} disabled={!selectedItem} class="text-destructive hover:bg-destructive/10">
|
||||
<Trash2 size={16} class="mr-2" /> Eliminar
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button variant="secondary" size="sm" onclick={handlePrint} disabled={!selectedItem}>
|
||||
Imprimir
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $page.url.searchParams.get('doda_id')}
|
||||
<DodaFormModal
|
||||
dodaIdParam={$page.url.searchParams.get('doda_id')!}
|
||||
onClose={() => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.delete('doda_id');
|
||||
goto(url.toString(), { replaceState: true });
|
||||
reloadData();
|
||||
}}
|
||||
onCreatedNavigateTo={(newId) => {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('doda_id', String(newId));
|
||||
goto(url.toString(), { replaceState: true });
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user