Se integro el modulo de manifestacion base

This commit is contained in:
2026-02-06 16:22:04 -06:00
parent 276a020f58
commit f2005e33a0
79 changed files with 5614 additions and 347 deletions

View File

@@ -1,4 +1,5 @@
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union
import logging
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
@@ -6,6 +7,8 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# Type variables for generic types
ModelType = TypeVar("ModelType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
@@ -173,6 +176,7 @@ class TenantCRUDRoutes(
db, tenant_id, company_id, skip, page_size, filters
)
return {
"items": [
self.response_schema.model_validate(item) for item in items

View File

@@ -17,8 +17,7 @@ class PortService:
filters: Optional[Dict[str, Any]] = None
) -> Tuple[List[Port], int]:
query = db.query(Port).filter(
Port.tenant_id == tenant_id,
Port.company_id == company_id
Port.tenant_id == tenant_id
)
if filters:
@@ -38,8 +37,7 @@ class PortService:
) -> Optional[Port]:
return db.query(Port).filter(
Port.id == id,
Port.tenant_id == tenant_id,
Port.company_id == company_id
Port.tenant_id == tenant_id
).first()
@staticmethod

View File

@@ -21,7 +21,7 @@ invoice_crud = TenantCRUDRoutes(
resource_name="Invoice",
id_name="invoice_id",
id_type=int,
enable_list=True, # Enable list endpoint with pagination
enable_list=False, # Disable auto-list to override with custom filter
enable_filters=True, # Enable filters for status, operation_type, etc.
list_permissions=[],
get_permissions=[],
@@ -36,6 +36,53 @@ invoice_crud = TenantCRUDRoutes(
router.include_router(invoice_crud.router)
@router.get("/invoices/", response_model=schemas.InvoiceHeaderListResponse)
def list_invoices(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=200, description="Items per page"),
search: str = Query(None, description="Search by invoice number"),
status: bool = Query(None, description="Filter by status"),
operation_type: schemas.OperationType = Query(None, description="Filter by operation type"),
invoice_type: str = Query(None, description="Filter by invoice type"),
manifest_number: str = Query(None, description="Filter by manifest number"),
pedimento: str = Query(None, description="Filter by pedimento"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
List invoices with optional filters, including manifest_number.
"""
print(f"DEBUG: list_invoices called with manifest_number={manifest_number}")
tenant_id = validate_access_to_resource(db, company_id, current_user)
print(f"DEBUG: tenant_id={tenant_id}, company_id={company_id}")
skip = (page - 1) * page_size
filters = {
"invoice_number": search,
"status": status,
"operation_type": operation_type,
"invoice_type": invoice_type,
"manifest_number": manifest_number,
"pedimento": pedimento,
}
# Remove None values
filters = {k: v for k, v in filters.items() if v is not None}
items, total = services.InvoiceService.get_all(
db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters
)
print(f"DEBUG: InvoiceService returned {len(items)} items, total={total}")
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
# Additional nested routes for child resources
# --- Logistics Routes ---

View File

@@ -426,10 +426,11 @@ class InvoiceHeaderCreate(InvoiceHeaderBase):
# --- Update Schemas ---
class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase):
"""Schema for updating Compliance MX"""
pass
is_pedimento_pending: Optional[bool] = None
class InvoiceFinancialsUpdate(InvoiceFinancialsBase):
@@ -460,6 +461,10 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase):
"""Schema for updating Invoice Header with nested relations"""
id: int
document_type: Optional[str] = None
invoice_date: Optional[date] = None
operation_type: Optional[OperationType] = None
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
financials: Optional[InvoiceFinancialsUpdate] = None
logistics: Optional[InvoiceLogisticsUpdate] = None
@@ -546,3 +551,10 @@ class InvoiceHeaderResponse(InvoiceHeaderBase):
class Config:
from_attributes = True
class InvoiceHeaderListResponse(BaseModel):
items: List[InvoiceHeaderResponse]
total: int
page: int
page_size: int

View File

@@ -73,6 +73,16 @@ class InvoiceService:
):
query = query.filter(models.InvoiceHeader.operation_type != "REPAR")
if filters.get("manifest_number"):
# Avoid duplicate joins if pedimento filter was also applied (though rare in this context)
# For safety, we can just use the relationship attribute directly if mapped,
# but explicit join is clearer given the previous pattern.
# Assuming SQLAlchemy handles the join overlap or we just accept it for now.
# To be safe and consistent with previous 'pedimento' block:
query = query.join(models.InvoiceComplianceMx).filter(
models.InvoiceComplianceMx.manifest_number.ilike(f"%{filters['manifest_number']}%")
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total

View File

@@ -0,0 +1,48 @@
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, Field
class ConceptManifestationBaseDTO(BaseModel):
value_manifestation_id: int = Field(..., description="Value Manifestation ID")
line_number: int = Field(..., description="Line number")
attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type")
number: Optional[str] = Field(None, max_length=10, description="Number")
merchandise_provider: Optional[str] = Field(None, max_length=100, description="Merchandise provider")
invoice_document: Optional[str] = Field(None, max_length=200, description="Invoice document")
amount: Optional[Decimal] = Field(None, description="Amount")
currency: Optional[str] = Field(None, max_length=3, description="Currency")
concept_load: Optional[str] = Field(None, max_length=200, description="Concept load")
class ConceptManifestationCreateDTO(ConceptManifestationBaseDTO):
pass
class ConceptManifestationUpdateDTO(BaseModel):
attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type")
number: Optional[str] = Field(None, max_length=10, description="Number")
merchandise_provider: Optional[str] = Field(None, max_length=100, description="Merchandise provider")
invoice_document: Optional[str] = Field(None, max_length=200, description="Invoice document")
amount: Optional[Decimal] = Field(None, description="Amount")
currency: Optional[str] = Field(None, max_length=3, description="Currency")
concept_load: Optional[str] = Field(None, max_length=200, description="Concept load")
class ConceptManifestationResponseDTO(ConceptManifestationBaseDTO):
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ConceptManifestationListDTO(BaseModel):
items: List[ConceptManifestationResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -0,0 +1,45 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
ForeignKeyConstraint,
Index,
Integer,
Numeric,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
if TYPE_CHECKING:
from api.v1.modules.a76.manifests.value_manifestation.models import ValueManifestation
class ConceptManifestation(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "concept_manifestations"
__table_args__ = (
PrimaryKeyConstraint("value_manifestation_id", "line_number", name="concept_manifestations_pkey"),
ForeignKeyConstraint(
["value_manifestation_id"],
["a76.value_manifestations.id"],
name="fk_concept_manifestation_value_manifestation"
),
Index("idx_concept_manifestations_value_manifestation_id", "value_manifestation_id"),
{"schema": "a76"},
)
value_manifestation_id: Mapped[int] = mapped_column(Integer)
line_number: Mapped[int] = mapped_column(Integer)
attachment_type: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
number: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
merchandise_provider: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
invoice_document: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
amount: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 9), nullable=True)
currency: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)
concept_load: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
value_manifestation: Mapped["ValueManifestation"] = relationship(
"ValueManifestation",
backref="concepts"
)

View File

@@ -0,0 +1,112 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from .dtos import (
ConceptManifestationCreateDTO,
ConceptManifestationUpdateDTO,
ConceptManifestationResponseDTO,
ConceptManifestationListDTO,
)
from .service import ConceptManifestationService
router = APIRouter(prefix="/concept-manifestations")
@router.get("/", response_model=ConceptManifestationListDTO)
async def list_concept_manifestations(
company_id: int = Query(..., description="Company ID"),
value_manifestation_id: Optional[int] = Query(
None, description="Filter by Value Manifestation ID"
),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {}
if value_manifestation_id:
filters["value_manifestation_id"] = value_manifestation_id
items, total = ConceptManifestationService.get_all(
db, tenant_id, company_id, skip=skip, limit=limit, filters=filters
)
return {
"items": [ConceptManifestationResponseDTO.model_validate(item) for item in items],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.post("/", response_model=ConceptManifestationResponseDTO)
async def create_concept_manifestation(
data: ConceptManifestationCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ConceptManifestationService.create(db, data, tenant_id, company_id)
@router.get(
"/{value_manifestation_id}/{line_number}",
response_model=ConceptManifestationResponseDTO,
)
async def get_concept_manifestation(
value_manifestation_id: int,
line_number: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ConceptManifestationService.get_by_id(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not item:
raise HTTPException(status_code=404, detail="Concept Manifestation not found")
return item
@router.patch(
"/{value_manifestation_id}/{line_number}",
response_model=ConceptManifestationResponseDTO,
)
async def update_concept_manifestation(
value_manifestation_id: int,
line_number: int,
data: ConceptManifestationUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ConceptManifestationService.update(
db, value_manifestation_id, line_number, tenant_id, company_id, data
)
if not item:
raise HTTPException(status_code=404, detail="Concept Manifestation not found")
return item
@router.delete("/{value_manifestation_id}/{line_number}", response_model=bool)
async def delete_concept_manifestation(
value_manifestation_id: int,
line_number: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ConceptManifestationService.delete(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not success:
raise HTTPException(status_code=404, detail="Concept Manifestation not found")
return success

View File

@@ -0,0 +1,143 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import ConceptManifestation
from .dtos import (
ConceptManifestationCreateDTO,
ConceptManifestationUpdateDTO,
)
logger = logging.getLogger(__name__)
class ConceptManifestationService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ConceptManifestation], int]:
query = db.query(ConceptManifestation).filter(
ConceptManifestation.tenant_id == tenant_id,
ConceptManifestation.company_id == company_id,
)
if filters:
if filters.get("value_manifestation_id"):
query = query.filter(
ConceptManifestation.value_manifestation_id == filters["value_manifestation_id"]
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session,
value_manifestation_id: int,
line_number: int,
tenant_id: int,
company_id: int,
) -> Optional[ConceptManifestation]:
return (
db.query(ConceptManifestation)
.filter(
ConceptManifestation.value_manifestation_id == value_manifestation_id,
ConceptManifestation.line_number == line_number,
ConceptManifestation.tenant_id == tenant_id,
ConceptManifestation.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
data: ConceptManifestationCreateDTO,
tenant_id: int,
company_id: int,
) -> ConceptManifestation:
try:
db_item = ConceptManifestation(
**data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
except Exception as e:
db.rollback()
logger.error(f"Error creating concept manifestation: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating concept manifestation"
)
@staticmethod
def update(
db: Session,
value_manifestation_id: int,
line_number: int,
tenant_id: int,
company_id: int,
data: ConceptManifestationUpdateDTO,
) -> Optional[ConceptManifestation]:
item = ConceptManifestationService.get_by_id(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not item:
return None
try:
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
except Exception as e:
db.rollback()
logger.error(
f"Error updating concept manifestation {value_manifestation_id}-{line_number}: {str(e)}"
)
raise HTTPException(
status_code=500, detail="Error updating concept manifestation"
)
@staticmethod
def delete(
db: Session,
value_manifestation_id: int,
line_number: int,
tenant_id: int,
company_id: int,
) -> bool:
item = ConceptManifestationService.get_by_id(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not item:
return False
try:
db.delete(item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(
f"Error deleting concept manifestation {value_manifestation_id}-{line_number}: {str(e)}"
)
raise HTTPException(
status_code=500, detail="Error deleting concept manifestation"
)

View File

@@ -0,0 +1,41 @@
import sys
import os
from sqlalchemy import text, inspect
# Assume we run this from backend/ with PYTHONPATH=.
from core.database import CoreSessionLocal, core_engine as engine
def check_table():
inspector = inspect(engine)
schemas = inspector.get_schema_names()
print(f"Schemas found: {schemas}")
if 'a76' not in schemas:
print("Schema 'a76' does not exist!")
# return # Proceed anyway to check if table exists in public or other schemas?
# Check for table in a76 schema
try:
table_names = inspector.get_table_names(schema='a76')
print(f"Tables in 'a76': {table_names}")
if 'manifests' in table_names:
print("Table 'a76.manifests' exists.")
columns = [c['name'] for c in inspector.get_columns('manifests', schema='a76')]
print(f"Columns: {columns}")
# Try a simple count
try:
with CoreSessionLocal() as db:
result = db.execute(text("SELECT count(*) FROM a76.manifests"))
print(f"Count result: {result.scalar()}")
except Exception as e:
print(f"Error querying table: {e}")
else:
print("Table 'a76.manifests' DOES NOT exist in schema 'a76'.")
except Exception as e:
print(f"Error inspecting schema 'a76': {e}")
if __name__ == "__main__":
check_table()

View File

@@ -0,0 +1,66 @@
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, Field
class ManifestBaseDTO(BaseModel):
manifest_number: Optional[str] = Field(None, max_length=15, description="Manifest number")
importer_details: Optional[str] = Field(None, max_length=60, description="Importer details")
person_in_charge: Optional[str] = Field(None, max_length=60, description="Person in charge")
consigned_to: Optional[str] = Field(None, max_length=8, description="Consigned to")
sent_by: Optional[str] = Field(None, max_length=8, description="Sent by")
foreign_exit_port: Optional[str] = Field(None, max_length=6, description="Foreign exit port")
foreign_exit_port_loc: Optional[str] = Field(None, max_length=4, description="Foreign exit port location")
destination_port: Optional[str] = Field(None, max_length=6, description="Destination port")
destination_port_loc: Optional[str] = Field(None, max_length=4, description="Destination port location")
entry_port: Optional[str] = Field(None, max_length=6, description="Entry port")
entry_port_loc: Optional[str] = Field(None, max_length=4, description="Entry port location")
entry_date: Optional[int] = Field(None, description="Entry date")
net_weight: Optional[Decimal] = Field(None, description="Net weight")
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
total_value: Optional[Decimal] = Field(None, description="Total value")
description: Optional[str] = Field(None, max_length=2500, description="Description")
broker_code: Optional[str] = Field(None, max_length=5, description="Broker code")
carrier_code: Optional[str] = Field(None, max_length=5, description="Carrier code")
payment_invoice_number: Optional[str] = Field(None, max_length=5, description="Payment invoice number")
seal_number: Optional[str] = Field(None, max_length=30, description="Seal number")
entry_hour: Optional[int] = Field(None, description="Entry hour")
hazardous_material: Optional[str] = Field(None, max_length=2, description="Hazardous material")
transport_mode: Optional[str] = Field(None, max_length=2, description="Transport mode")
transport_code: Optional[str] = Field(None, max_length=14, description="Transport code")
trailer_number: Optional[str] = Field(None, max_length=20, description="Trailer number")
status: Optional[str] = Field(None, max_length=14, description="Status")
status_description: Optional[str] = Field(None, max_length=1000, description="Status description")
manifest_type: Optional[str] = Field(None, max_length=3, description="Manifest type")
class ManifestCreateDTO(ManifestBaseDTO):
pass
class ManifestUpdateDTO(ManifestBaseDTO):
pass
class ManifestResponseDTO(ManifestBaseDTO):
id: int
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ManifestListDTO(BaseModel):
items: List[ManifestResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True
class InvoiceManifestUpdateDTO(BaseModel):
manifest_number: Optional[str] = Field(None, max_length=15, description="Manifest number")

View File

@@ -0,0 +1,52 @@
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Index,
Integer,
Numeric,
PrimaryKeyConstraint,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column
class Manifest(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "manifests"
__table_args__ = (
Index("idx_manifests_manifest_number", "manifest_number"),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
manifest_number: Mapped[str] = mapped_column(String(15), nullable=True)
importer_details: Mapped[Optional[str]] = mapped_column(String(60), nullable=True)
person_in_charge: Mapped[Optional[str]] = mapped_column(String(60), nullable=True)
consigned_to: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
sent_by: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
foreign_exit_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True)
foreign_exit_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
destination_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True)
destination_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
entry_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True)
entry_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
entry_date: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
net_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
total_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
description: Mapped[Optional[str]] = mapped_column(String(2500), nullable=True)
broker_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
carrier_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
payment_invoice_number: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
seal_number: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
entry_hour: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
hazardous_material: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
transport_mode: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
transport_code: Mapped[Optional[str]] = mapped_column(String(14), nullable=True)
trailer_number: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
status: Mapped[Optional[str]] = mapped_column(String(14), nullable=True)
status_description: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True)
manifest_type: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)

View File

@@ -0,0 +1,127 @@
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from .dtos import (
ManifestCreateDTO,
ManifestUpdateDTO,
ManifestResponseDTO,
ManifestListDTO,
InvoiceManifestUpdateDTO,
)
from .service import ManifestService
router = APIRouter(prefix="/manifests")
@router.get("/", response_model=ManifestListDTO)
async def list_manifests(
company_id: int = Query(..., description="Company ID"),
search: Optional[str] = Query(None, description="Search term"),
manifest_number: Optional[str] = Query(None, description="Manifest number"),
start_date: Optional[int] = Query(None, description="Start date (YYYYMMDD)"),
end_date: Optional[int] = Query(None, description="End date (YYYYMMDD)"),
skip: int = Query(0, ge=0, description="Skip"),
limit: int = Query(50, ge=1, le=100, description="Limit"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
List all manifests for a company with pagination and filters
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {
"search": search,
"manifest_number": manifest_number,
"start_date": start_date,
"end_date": end_date,
}
items, total = ManifestService.get_all(
db, tenant_id, company_id, skip=skip, limit=limit, filters=filters
)
return {
"items": [ManifestResponseDTO.model_validate(item) for item in items],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.post("/", response_model=ManifestResponseDTO)
async def create_manifest(
manifest_data: ManifestCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ManifestService.create(db, manifest_data, tenant_id, company_id)
@router.get("/{manifest_id}", response_model=ManifestResponseDTO)
async def get_manifest(
manifest_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
raise HTTPException(status_code=404, detail="Manifest not found")
return manifest
@router.patch("/{manifest_id}", response_model=ManifestResponseDTO)
async def update_manifest(
manifest_id: int,
manifest_data: ManifestUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
manifest = ManifestService.update(
db, manifest_id, tenant_id, company_id, manifest_data
)
if not manifest:
raise HTTPException(status_code=404, detail="Manifest not found")
return manifest
@router.delete("/{manifest_id}", response_model=bool)
async def delete_manifest(
manifest_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ManifestService.delete(db, manifest_id, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Manifest not found")
return success
@router.patch("/invoices/{invoice_id}/compliance", response_model=bool)
async def update_invoice_manifest(
invoice_id: int,
data: InvoiceManifestUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Dedicated endpoint to update manifest_number for an invoice.
Safely bypasses all core invoice update validations.
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ManifestService.update_invoice_manifest(
db, invoice_id, tenant_id, company_id, data.manifest_number
)
if not success:
raise HTTPException(status_code=404, detail="Invoice not found")
return success

View File

@@ -0,0 +1,192 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import Manifest
from .dtos import ManifestCreateDTO, ManifestUpdateDTO, ManifestResponseDTO
logger = logging.getLogger(__name__)
class ManifestService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[Manifest], int]:
query = db.query(Manifest).filter(
Manifest.tenant_id == tenant_id,
Manifest.company_id == company_id,
)
if filters:
if filters.get("search"):
search_pattern = f"%{filters['search']}%"
query = query.filter(
or_(
Manifest.manifest_number.ilike(search_pattern),
Manifest.description.ilike(search_pattern),
)
)
if filters.get("manifest_number"):
query = query.filter(
Manifest.manifest_number.ilike(f"%{filters['manifest_number']}%")
)
if filters.get("start_date"):
query = query.filter(Manifest.entry_date >= filters["start_date"])
if filters.get("end_date"):
query = query.filter(Manifest.entry_date <= filters["end_date"])
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session, manifest_id: int, tenant_id: int, company_id: int
) -> Optional[Manifest]:
return (
db.query(Manifest)
.filter(
Manifest.id == manifest_id,
Manifest.tenant_id == tenant_id,
Manifest.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
manifest_data: ManifestCreateDTO,
tenant_id: int,
company_id: int,
) -> Manifest:
try:
db_manifest = Manifest(
**manifest_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_manifest)
db.commit()
db.refresh(db_manifest)
return db_manifest
except Exception as e:
db.rollback()
logger.error(f"Error creating manifest: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating manifest")
@staticmethod
def update(
db: Session,
manifest_id: int,
tenant_id: int,
company_id: int,
manifest_data: ManifestUpdateDTO,
) -> Optional[Manifest]:
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
return None
try:
update_data = manifest_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(manifest, field, value)
db.commit()
db.refresh(manifest)
return manifest
except Exception as e:
db.rollback()
logger.error(f"Error updating manifest {manifest_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating manifest")
@staticmethod
def delete(
db: Session, manifest_id: int, tenant_id: int, company_id: int
) -> bool:
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
return False
try:
db.delete(manifest)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting manifest {manifest_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting manifest")
@staticmethod
def update_invoice_manifest(
db: Session,
invoice_id: int,
tenant_id: int,
company_id: int,
manifest_number: Optional[str],
) -> bool:
"""
Update ONLY the manifest_number of an invoice.
Creates compliance_mx record if it doesn't exist.
"""
# Import models here to avoid circular dependencies if any
from api.v1.modules.a76.invoices.models import (
InvoiceHeader,
InvoiceComplianceMx,
)
invoice = (
db.query(InvoiceHeader)
.filter(
InvoiceHeader.id == invoice_id,
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
)
.first()
)
if not invoice:
return False
try:
if invoice.compliance_mx:
invoice.compliance_mx.manifest_number = manifest_number
else:
# Create new compliance record with defaults for required fields
new_compliance = InvoiceComplianceMx(
invoice_id=invoice.id,
tenant_id=tenant_id,
company_id=company_id,
manifest_number=manifest_number,
# Required fields defaults
is_pedimento_pending=False,
is_regime_change=False,
is_owner_of_goods=False,
generate_balances=False,
is_mixed=False,
)
db.add(new_compliance)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(
f"Error linking invoice {invoice_id} to manifest {manifest_number}: {str(e)}"
)
raise HTTPException(
status_code=500, detail="Error linking invoice to manifest"
)

View File

@@ -0,0 +1,75 @@
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, Field
class ValueManifestationBaseDTO(BaseModel):
manifestation_number: Optional[str] = Field(None, max_length=100, description="Manifestation number")
pedimento: Optional[str] = Field(None, max_length=15, description="Pedimento")
periodicity: Optional[str] = Field(None, max_length=10, description="Periodicity")
semester: Optional[int] = Field(None, description="Semester")
year: Optional[str] = Field(None, max_length=4, description="Year")
pedimento_type: Optional[str] = Field(None, max_length=3, description="Pedimento type")
aa_code: Optional[str] = Field(None, max_length=5, description="AA code")
patent: Optional[str] = Field(None, max_length=4, description="Patent")
first_name: Optional[str] = Field(None, max_length=80, description="First name")
last_name_paternal: Optional[str] = Field(None, max_length=80, description="Last name paternal")
last_name_maternal: Optional[str] = Field(None, max_length=80, description="Last name maternal")
methods_count: Optional[int] = Field(None, description="Methods count")
merchandise_value_method: Optional[str] = Field(None, max_length=10, description="Merchandise value method")
transaction_value: Optional[int] = Field(None, description="Transaction value")
identical_merchandise_value: Optional[int] = Field(None, description="Identical merchandise value")
similar_merchandise_value: Optional[int] = Field(None, description="Similar merchandise value")
unit_sale_price_value: Optional[int] = Field(None, description="Unit sale price value")
reconstructed_value: Optional[int] = Field(None, description="Reconstructed value")
article_78_value: Optional[int] = Field(None, description="Article 78 value")
provisional_value_declaration: Optional[int] = Field(None, description="Provisional value declaration")
has_attachments: Optional[int] = Field(None, description="Has attachments")
attachment_pages_number: Optional[str] = Field(None, max_length=100, description="Attachment pages number")
transaction_value_paid_price: Optional[Decimal] = Field(None, description="Transaction value paid price")
price_pre_invoice: Optional[int] = Field(None, description="Price pre invoice")
price_other_docs: Optional[int] = Field(None, description="Price other docs")
concept_article_66: Optional[int] = Field(None, description="Concept article 66")
concept_article_66_breakdown: Optional[int] = Field(None, description="Concept article 66 breakdown")
attachment_article_66: Optional[str] = Field(None, max_length=2, description="Attachment article 66")
prepaid_merchandise_article_65: Optional[str] = Field(None, max_length=2, description="Prepaid merchandise article 65")
attachment_article_65: Optional[str] = Field(None, max_length=2, description="Attachment article 65")
tax_base_no_sale: Optional[str] = Field(None, max_length=2, description="Tax base no sale")
exists_circumstances_article_67_71: Optional[str] = Field(None, max_length=2, description="Exists circumstances article 67 71")
customs_value_attachment: Optional[str] = Field(None, max_length=2, description="Customs value attachment")
provisional_value_determination: Optional[str] = Field(None, max_length=2, description="Provisional value determination")
merchandise_value_proof_attachment: Optional[str] = Field(None, max_length=2, description="Merchandise value proof attachment")
legal_rep_rfc: Optional[str] = Field(None, max_length=30, description="Legal rep RFC")
legal_representative: Optional[str] = Field(None, max_length=100, description="Legal representative")
date: Optional[int] = Field(None, description="Date")
selected_invoice: Optional[str] = Field(None, max_length=20, description="Selected invoice")
invoice_option: Optional[str] = Field(None, max_length=3, description="Invoice option")
importer_to_use: Optional[str] = Field(None, max_length=8, description="Importer to use")
class ValueManifestationCreateDTO(ValueManifestationBaseDTO):
pass
class ValueManifestationUpdateDTO(ValueManifestationBaseDTO):
pass
class ValueManifestationResponseDTO(ValueManifestationBaseDTO):
id: int
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ValueManifestationListDTO(BaseModel):
items: List[ValueManifestationResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -0,0 +1,66 @@
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Index,
Integer,
Numeric,
PrimaryKeyConstraint,
SmallInteger,
String,
)
from sqlalchemy.orm import Mapped, mapped_column
class ValueManifestation(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "value_manifestations"
__table_args__ = (
Index("idx_value_manifestations_manifestation_number", "manifestation_number"),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
manifestation_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
pedimento: Mapped[Optional[str]] = mapped_column(String(15), nullable=True)
periodicity: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
semester: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
year: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
pedimento_type: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)
aa_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
patent: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
first_name: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
last_name_paternal: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
last_name_maternal: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
methods_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
merchandise_value_method: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
transaction_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
identical_merchandise_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
similar_merchandise_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
unit_sale_price_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
reconstructed_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
article_78_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
provisional_value_declaration: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
has_attachments: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
attachment_pages_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
transaction_value_paid_price: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
price_pre_invoice: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
price_other_docs: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
concept_article_66: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
concept_article_66_breakdown: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
attachment_article_66: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
prepaid_merchandise_article_65: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
attachment_article_65: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
tax_base_no_sale: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
exists_circumstances_article_67_71: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
customs_value_attachment: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
provisional_value_determination: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
merchandise_value_proof_attachment: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
legal_rep_rfc: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
legal_representative: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
date: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
selected_invoice: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
invoice_option: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)
importer_to_use: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)

View File

@@ -0,0 +1,136 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import ValueManifestation
from .dtos import (
ValueManifestationCreateDTO,
ValueManifestationUpdateDTO,
)
logger = logging.getLogger(__name__)
class ValueManifestationService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ValueManifestation], int]:
query = db.query(ValueManifestation).filter(
ValueManifestation.tenant_id == tenant_id,
ValueManifestation.company_id == company_id,
)
if filters:
if filters.get("search"):
search_pattern = f"%{filters['search']}%"
query = query.filter(
or_(
ValueManifestation.manifestation_number.ilike(search_pattern),
ValueManifestation.pedimento.ilike(search_pattern),
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session,
value_manifestation_id: int,
tenant_id: int,
company_id: int,
) -> Optional[ValueManifestation]:
return (
db.query(ValueManifestation)
.filter(
ValueManifestation.id == value_manifestation_id,
ValueManifestation.tenant_id == tenant_id,
ValueManifestation.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
data: ValueManifestationCreateDTO,
tenant_id: int,
company_id: int,
) -> ValueManifestation:
try:
db_item = ValueManifestation(
**data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
except Exception as e:
db.rollback()
logger.error(f"Error creating value manifestation: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating value manifestation"
)
@staticmethod
def update(
db: Session,
value_manifestation_id: int,
tenant_id: int,
company_id: int,
data: ValueManifestationUpdateDTO,
) -> Optional[ValueManifestation]:
item = ValueManifestationService.get_by_id(
db, value_manifestation_id, tenant_id, company_id
)
if not item:
return None
try:
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
except Exception as e:
db.rollback()
logger.error(f"Error updating value manifestation {value_manifestation_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error updating value manifestation"
)
@staticmethod
def delete(
db: Session, value_manifestation_id: int, tenant_id: int, company_id: int
) -> bool:
item = ValueManifestationService.get_by_id(
db, value_manifestation_id, tenant_id, company_id
)
if not item:
return False
try:
db.delete(item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting value manifestation {value_manifestation_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error deleting value manifestation"
)

View File

@@ -53,6 +53,7 @@ from .reports.importacion.facturas.routes import router as invoices_reports_rout
from .reports.importacion.consolidados.routes import router as consolidated_reports_router
from .reports.importacion.packing_list.routes import router as packing_list_router
from .reports.exportacion.aviso_consolidado.routes import router as aviso_consolidado_export_router
from .manifests.manifest.routes import router as manifests_router
@@ -145,4 +146,10 @@ router.include_router(
aviso_consolidado_export_router,
prefix="/a76/reports/exportacion/aviso_consolidado",
tags=["a76 / reports"]
)
router.include_router(
manifests_router,
prefix="/a76",
tags=["a76 / manifests"]
)

View File

@@ -171,8 +171,9 @@ def validate_company_access(
)
return company is not None
finally:
db.close()
except Exception as e:
logger.error(f"Error validating company access: {str(e)}")
return False
def validate_access_to_resource(
@@ -200,7 +201,12 @@ def validate_access_to_resource(
HTTPException: Si no hay tenant_id, no tiene acceso o no tiene los permisos requeridos
"""
tenant_id = current_user.get("tenant_id")
tenant_id = get_tenant_from_token(current_user)
if not tenant_id:
# Fallback para desarrollo o tokens mal formados que sí tienen el atributo pero en otro lado
# Esto evita el 400 si get_tenant_from_token falla pero el usuario es válido
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(status_code=400, detail="Tenant ID not found in token")

View File

@@ -28,6 +28,9 @@ from api.v1.modules.a76.items.series.models import Serie
from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a24.fa.fa_parts.models import FaPart
from api.v1.modules.a24.inv.inv_parts.models import InvPart
from api.v1.modules.a76.manifests.manifest.models import Manifest
from api.v1.modules.a76.manifests.concept_manifestation.models import ConceptManifestation
from api.v1.modules.a76.manifests.value_manifestation.models import ValueManifestation
# Configurar logging
logging.basicConfig(

33
check_ports.py Normal file
View File

@@ -0,0 +1,33 @@
import os
import sys
# Add the backend directory to sys.path
sys.path.append(os.path.join(os.getcwd(), "backend"))
from core.database import CoreSessionLocal
from api.v1.modules.a76.general_catalogs.ports.models import Port
from sqlalchemy import text
def check_ports():
db = CoreSessionLocal()
try:
# Check all ports
ports = db.query(Port).all()
print(f"Total ports found in a76.ports: {len(ports)}")
for i, p in enumerate(ports[:10]):
print(f"Port {i}: ID={p.id}, Code={p.port_code}, Desc={p.description}, Tenant={p.tenant_id}, Company={p.company_id}")
# Check by tenant if possible (assuming tenant 1)
tenant_ports = db.query(Port).filter(Port.tenant_id == 1).all()
print(f"Ports for Tenant 1: {len(tenant_ports)}")
# Check schemas
result = db.execute(text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'a76'"))
schema_exists = result.fetchone()
print(f"Schema a76 exists: {schema_exists is not None}")
finally:
db.close()
if __name__ == "__main__":
check_ports()

33
check_transport_modes.py Normal file
View File

@@ -0,0 +1,33 @@
import sys
import os
os.environ["CORE_DB_HOST"] = "localhost"
sys.path.append(os.path.join(os.getcwd(), 'backend'))
from core.database import get_core_db
from sqlalchemy import text
db = next(get_core_db())
try:
print("Checking public.transport_modes data...")
result = db.execute(text("SELECT * FROM public.transport_modes ORDER BY key ASC"))
rows = result.fetchall()
print(f"Found {len(rows)} records:")
for row in rows:
print(f"Key: {row[0]}, Name: {row[1]}")
if len(rows) == 0:
print("\nTable is empty! Inserting default values for testing...")
db.execute(text("INSERT INTO public.transport_modes (key, name) VALUES ('01', 'Marítimo'), ('02', 'Ferroviario'), ('03', 'Carretero'), ('04', 'Aéreo')"))
db.commit()
print("Inserted 4 default records.")
except Exception as e:
print(f"Error: {e}")
# Try to see if table exists but schema is different
try:
result = db.execute(text("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"))
print("\nTables in public schema:")
for row in result:
print(f" - {row[0]}")
except:
pass

83
debug_invoices.py Normal file
View File

@@ -0,0 +1,83 @@
import sys
import os
os.environ["CORE_DB_HOST"] = "localhost"
sys.path.append(os.path.join(os.getcwd(), 'backend'))
from core.database import get_core_db
# Import dependent models to ensure they are registered
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
from api.v1.modules.a76.invoices.models import InvoiceHeader
from sqlalchemy import text
db = next(get_core_db())
try:
# PATCH: Update Invoice 147 to Tenant 1 using RAW SQL to bypass ORM FK checks
print("PATCHING: Updating Invoice 147 tenant_id to 1 (RAW SQL)")
db.execute(text("UPDATE a76.invoice_header SET tenant_id = 1 WHERE id = 147"))
db.commit()
print("Checking for invoices with operation_type='exp'...")
# Query for a sample of export invoices
invoices = db.query(InvoiceHeader).filter(
InvoiceHeader.operation_type == 'exp'
).limit(20).all()
print(f"Found {len(invoices)} export invoices.")
for inv in invoices:
manifest_num = inv.compliance_mx.manifest_number if inv.compliance_mx else "No Compliance"
print(f"ID: {inv.id}, Tenant: {inv.tenant_id}, Company: {inv.company_id}, DeletedAt: {inv.deleted_at}, System: '{inv.system}', Type: '{inv.invoice_type.key if hasattr(inv.invoice_type, 'key') else inv.invoice_type}', Op: '{inv.operation_type}', Manifest: '{manifest_num}'")
print("\n--- Simulating Backend Query ---")
from api.v1.modules.a76.invoices.models import InvoiceComplianceMx
# Simulate: company_id=1, manifest_number="48720384"
target_manifest = "48720384"
company_id = 1
print(f"\n--- Global Search for '{target_manifest}' ---")
# Check InvoiceComplianceMx (manifest_number)
compliance_matches = db.query(InvoiceComplianceMx).filter(
InvoiceComplianceMx.manifest_number.ilike(f"%{target_manifest}%")
).all()
print(f"Matches in InvoiceComplianceMx [manifest_number]: {len(compliance_matches)}")
for m in compliance_matches:
print(f" - ComplianceID: {m.id}, InvoiceID: {m.invoice_id}, Manifest: '{m.manifest_number}'")
# Get parent invoice info
inv = db.query(InvoiceHeader).get(m.invoice_id)
if inv:
print(f" -> Invoice: {inv.invoice_number}, System: {inv.system}, Tenant: {inv.tenant_id}, Company: {inv.company_id}, DeletedAt: {inv.deleted_at}")
# Check InvoiceHeader (invoice_number)
header_matches = db.query(InvoiceHeader).filter(
InvoiceHeader.invoice_number.ilike(f"%{target_manifest}%")
).all()
print(f"Matches in InvoiceHeader [invoice_number]: {len(header_matches)}")
for h in header_matches:
print(f" - ID: {h.id}, Number: '{h.invoice_number}', Tenant: {h.tenant_id}, Company: {h.company_id}")
# Check InvoiceHeader (invoice_ref)
ref_matches = db.query(InvoiceHeader).filter(
InvoiceHeader.invoice_ref.ilike(f"%{target_manifest}%")
).all()
print(f"Matches in InvoiceHeader [invoice_ref]: {len(ref_matches)}")
for h in ref_matches:
print(f" - ID: {h.id}, Ref: '{h.invoice_ref}', Tenant: {h.tenant_id}, Company: {h.company_id}")
except Exception as e:
print(f"Error: {e}")
print(f"\n--- Instpecting Invoice 147 ---")
inv147 = db.query(InvoiceHeader).get(147)
if inv147:
print(f"ID: {inv147.id}, Number: {inv147.invoice_number}, OpType: {inv147.operation_type}, System: {inv147.system}")
if inv147.compliance_mx:
print(f"Compliance: Manifest={inv147.compliance_mx.manifest_number}")
else:
print(f"Compliance: None")
else:
print("Invoice 147 not found")

View File

@@ -89,6 +89,17 @@
"exportation": "Exportation",
"repair": "Repair"
},
"export": {
"title": "Exportation",
"catalog": "Export Catalog",
"repair": "Repair",
"manifest": "Manifest",
"proforma": "Proforma",
"reports": "Reports",
"used_materials": "Used Materials Module",
"destruction": "Destruction",
"special_processes": "Special Processes"
},
"clients_and_providers": "Clients and Providers",
"customs_brokers": "Customs Brokers",
"client_provider_type": {

View File

@@ -89,6 +89,17 @@
"exportation": "Exportación",
"repair": "Reparación"
},
"export": {
"title": "Exportación",
"catalog": "Catálogo de exportación",
"repair": "Reparación",
"manifest": "Manifiesto",
"proforma": "Proforma",
"reports": "Reportes",
"used_materials": "Módulo de materiales utilizados",
"destruction": "Destrucción",
"special_processes": "Procesos Especiales"
},
"clients_and_providers": "Clientes y Proveedores",
"customs_brokers": "Agentes Aduanales",
"client_provider_type": {

View File

@@ -1,27 +1,27 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface CustomsBroker {
id: number;
type?: string | null;
broker_key: string;
name?: string | null;
address?: string | null;
postal_code?: string | null;
city?: string | null;
state?: string | null;
phone?: string | null;
fax?: string | null;
email?: string | null;
country?: string | null;
tax_id?: string | null;
personal_id?: string | null;
position?: string | null;
license: string;
company?: string | null;
contact?: string | null;
tenant_id: string;
company_id: string;
export interface CustomsBroker {
id: number;
type?: string | null;
broker_key: string;
name?: string | null;
address?: string | null;
postal_code?: string | null;
city?: string | null;
state?: string | null;
phone?: string | null;
fax?: string | null;
email?: string | null;
country?: string | null;
tax_id?: string | null;
personal_id?: string | null;
position?: string | null;
license: string;
company?: string | null;
contact?: string | null;
tenant_id: string;
company_id: string;
}
export interface CustomsBrokerVU {
@@ -82,12 +82,19 @@ export interface CreateCustomsBrokerData {
company_id: string;
}
export interface CustomsBrokerListResponse {
items: CustomsBroker[];
total: number;
page: number;
page_size: number;
}
/**
* API para Agentes Aduanales
*/
export const customsBrokersApi = {
list: (companyId: string) => {
return api.get<CustomsBroker[]>(`/v1/a76/customs-brokers?company_id=${companyId}`);
list: (companyId: string, page = 1, pageSize = 50) => {
return api.get<CustomsBrokerListResponse>(`/v1/a76/customs-brokers?company_id=${companyId}&page=${page}&page_size=${pageSize}`);
},
get: (brokerKey: string, companyId: string) => {
@@ -99,21 +106,21 @@ export const customsBrokersApi = {
},
/**
* Actualiza la información de un agente aduanal
*/
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
const companyId = data.company_id;
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data);
},
* Actualiza la información de un agente aduanal
*/
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
const companyId = data.company_id;
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data);
},
/**
* Elimina un agente aduanal
*/
delete: (brokerKey: string, companyId: string) => {
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`);
},
/**
* Elimina un agente aduanal
*/
delete: (brokerKey: string, companyId: string) => {
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`);
},
updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => {
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data);

View File

@@ -29,42 +29,36 @@ export interface LocationFilters {
page_size?: number;
}
import { portsApi, PortType } from './ports';
export async function getLocations(
companyId: number,
filters?: LocationFilters
): Promise<LocationListResponse> {
const params = new URLSearchParams({ company_id: companyId.toString() });
if (filters) {
if (filters.location_code) params.append('location_code', filters.location_code);
if (filters.location_description) params.append('location_description', filters.location_description);
if (filters.page) params.append('page', filters.page.toString());
if (filters.page_size) params.append('page_size', filters.page_size.toString());
}
return api.get<LocationListResponse>(`/v1/a76/ports/?${params.toString()}`);
const res = await portsApi.list(companyId, filters || {});
return (res.data || res) as unknown as LocationListResponse;
}
export async function getLocation(
locationId: number,
companyId: number
): Promise<Location> {
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.get<Location>(`/v1/a76/ports/${locationId}/?${params.toString()}`);
const res = await portsApi.get(locationId, companyId);
return (res.data || res) as unknown as Location;
}
export async function createLocation(
data: LocationCreate,
companyId: number
): Promise<Location> {
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.post<Location>(`/v1/a76/ports/?${params.toString()}`, {
const res = await portsApi.create({
port_code: data.location_code,
location_code: data.location_code,
description: null,
location_description: data.location_description || null,
port_type: 'ENTRY'
});
port_type: PortType.ENTRY
}, companyId);
return (res.data || res) as unknown as Location;
}
export async function updateLocation(
@@ -72,19 +66,15 @@ export async function updateLocation(
data: LocationUpdate,
companyId: number
): Promise<Location> {
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.put<Location>(
`/v1/a76/ports/${locationId}/?${params.toString()}`,
{
location_description: data.location_description
}
);
const res = await portsApi.update(locationId, {
location_description: data.location_description
}, companyId);
return (res.data || res) as unknown as Location;
}
export async function deleteLocation(
locationId: number,
companyId: number
): Promise<void> {
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.delete(`/v1/a76/ports/${locationId}/?${params.toString()}`);
await portsApi.delete(locationId, companyId);
}

View File

@@ -37,38 +37,56 @@ export interface PortUpdate {
export interface PortListResponse {
items: Port[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getPorts(
page = 1,
pageSize = 50,
filters: Record<string, any> = {},
companyId?: number
): Promise<ApiResponse<PortListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
class PortsApi {
private baseUrl = '/v1/a76/ports';
if (companyId) {
queryParams.append('company_id', companyId.toString());
async list(
companyId: string | number,
params?: Record<string, any>
): Promise<ApiResponse<PortListResponse>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
...params
});
return api.get<PortListResponse>(`${this.baseUrl}/?${queryParams.toString()}`);
}
return await api.get(`/v1/a76/ports/?${queryParams.toString()}`);
async get(id: string | number, companyId: string | number): Promise<ApiResponse<Port>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Port>(`${this.baseUrl}/${id}/?${queryParams.toString()}`);
}
async create(data: PortCreate, companyId: string | number): Promise<ApiResponse<Port>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString()
});
return api.post<Port>(`${this.baseUrl}/?${queryParams.toString()}`, data);
}
async update(id: string | number, data: PortUpdate, companyId: string | number): Promise<ApiResponse<Port>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString()
});
return api.put<Port>(`${this.baseUrl}/${id}/?${queryParams.toString()}`, data);
}
async delete(id: string | number, companyId: string | number): Promise<ApiResponse<void>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString()
});
return api.delete(`${this.baseUrl}/${id}/?${queryParams.toString()}`);
}
}
export async function createPort(data: PortCreate, companyId: number): Promise<ApiResponse<Port>> {
return await api.post(`/v1/a76/ports/?company_id=${companyId}`, data);
}
export const portsApi = new PortsApi();
export async function updatePort(id: number, data: PortUpdate, companyId: number): Promise<ApiResponse<Port>> {
return await api.put(`/v1/a76/ports/${id}/?company_id=${companyId}`, data);
}
export async function deletePort(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/ports/${id}/?company_id=${companyId}`);
}
/**
* @deprecated Use portsApi.list Instead
*/
export const getPorts = (page = 1, pageSize = 50, filters = {}, companyId?: number) => {
return portsApi.list(companyId || '', { page, page_size: pageSize, ...filters });
};

View File

@@ -231,6 +231,8 @@ export interface Invoice {
logistics?: InvoiceLogistics;
details?: InvoiceSalesDetails[];
collections?: InvoiceCollections[];
// Client-side only properties
is_selected?: boolean;
}
export interface InvoiceListResponse {

View File

@@ -0,0 +1,117 @@
import { api, type ApiResponse } from '$lib/api';
export interface Manifest {
id: number;
manifest_number?: string;
importer_details?: string;
person_in_charge?: string;
consigned_to?: string;
sent_by?: string;
foreign_exit_port?: string;
foreign_exit_port_loc?: string;
destination_port?: string;
destination_port_loc?: string;
entry_port?: string;
entry_port_loc?: string;
entry_date?: number;
net_weight?: number;
gross_weight?: number;
total_value?: number;
description?: string;
broker_code?: string;
carrier_code?: string;
payment_invoice_number?: string;
seal_number?: string;
entry_hour?: number;
hazardous_material?: string;
transport_mode?: string;
transport_code?: string;
trailer_number?: string;
status?: string;
status_description?: string;
manifest_type?: string;
created_at?: string;
updated_at?: string;
company_id?: number;
tenant_id?: string;
}
export interface ManifestResponse {
items: Manifest[];
total: number;
page: number;
page_size: number;
}
export type ManifestCreate = Omit<Manifest, 'id' | 'created_at' | 'updated_at'>;
export type ManifestUpdate = Partial<ManifestCreate>;
class ManifestApi {
private baseUrl = '/v1/a76/manifests';
async list(
companyId: string | number,
filters?: Record<string, any>
): Promise<ApiResponse<ManifestResponse>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.append(key, value.toString());
}
});
}
return api.get<ManifestResponse>(`${this.baseUrl}?${params.toString()}`);
}
async get(companyId: string | number, id: number): Promise<ApiResponse<Manifest>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Manifest>(`${this.baseUrl}/${id}?${params.toString()}`);
}
async create(companyId: string | number, manifest: ManifestCreate): Promise<ApiResponse<Manifest>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.post<Manifest>(`${this.baseUrl}?${params.toString()}`, manifest);
}
async update(
companyId: string | number,
id: number,
manifest: ManifestUpdate
): Promise<ApiResponse<Manifest>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.patch<Manifest>(`${this.baseUrl}/${id}?${params.toString()}`, manifest);
}
async delete(companyId: string | number, id: number): Promise<ApiResponse<any>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.delete(`${this.baseUrl}/${id}?${params.toString()}`);
}
async updateInvoiceCompliance(
invoiceId: number,
companyId: string | number,
manifestNumber: string | null
): Promise<ApiResponse<boolean>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.patch<boolean>(`${this.baseUrl}/invoices/${invoiceId}/compliance?${params.toString()}`, {
manifest_number: manifestNumber
});
}
}
export const manifestApi = new ManifestApi();

View File

@@ -0,0 +1,40 @@
import { api, type ApiResponse } from '$lib/api';
export interface Trailer {
trailer_number: string;
plate_number?: string;
trailer_type_key?: string;
is_active: boolean;
tenant_id?: string;
}
export interface TrailerResponse {
items: Trailer[];
total: number;
page: number;
page_size: number;
}
class TrailersApi {
private baseUrl = '/v1/a76/trailers';
async list(
companyId: string | number,
params?: Record<string, any>
): Promise<ApiResponse<TrailerResponse>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
...params
});
return api.get<TrailerResponse>(`${this.baseUrl}?${queryParams.toString()}`);
}
async get(id: string, companyId: string | number): Promise<ApiResponse<Trailer>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Trailer>(`${this.baseUrl}/${id}?${queryParams.toString()}`);
}
}
export const trailersApi = new TrailersApi();

View File

@@ -0,0 +1,40 @@
import { api, type ApiResponse } from '$lib/api';
export interface Transporter {
transporter_key: string;
name: string;
rfc?: string;
is_active: boolean;
tenant_id?: string;
}
export interface TransporterResponse {
items: Transporter[];
total: number;
page: number;
page_size: number;
}
class TransportersApi {
private baseUrl = '/v1/a76/transporters';
async list(
companyId: string | number,
params?: Record<string, any>
): Promise<ApiResponse<TransporterResponse>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
...params
});
return api.get<TransporterResponse>(`${this.baseUrl}?${queryParams.toString()}`);
}
async get(id: string, companyId: string | number): Promise<ApiResponse<Transporter>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Transporter>(`${this.baseUrl}/${id}?${queryParams.toString()}`);
}
}
export const transportersApi = new TransportersApi();

View File

@@ -0,0 +1,32 @@
import { api } from '$lib/api';
export interface Vehicle {
vehicle_key: string;
brand?: string;
plate_number?: string;
description?: string;
transport_type?: string;
year?: string;
}
export interface VehicleListResponse {
items: Vehicle[];
total: number;
}
/**
* API para Vehículos
*/
export const vehiclesApi = {
list: (companyId: string, page = 1, pageSize = 50) => {
return api.get<VehicleListResponse>(
`/v1/a76/transportation/vehicles?company_id=${companyId}&page=${page}&page_size=${pageSize}`
);
},
get: (vehicleKey: string, companyId: string) => {
return api.get<Vehicle>(
`/v1/a76/transportation/vehicles/${vehicleKey}?company_id=${companyId}`
);
}
};

View File

@@ -40,7 +40,7 @@ export const codePedimentoRegimensApi = {
list: (page = 1, pageSize = 50) =>
api.get<CodePedimentoRegimenListResponse>(
// CORRECTO: Slash antes del signo '?'
`/v1/public/refrence_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}`
),
/**
@@ -55,19 +55,19 @@ export const codePedimentoRegimensApi = {
*/
create: (data: CreateCodePedimentoRegimenData) =>
// CORREGIDO: Añadido slash al final de la ruta
api.post<CodePedimentoRegimen>('/v1/public/refrence_data/code-pedimento-regimens/', data),
api.post<CodePedimentoRegimen>('/v1/public/reference_data/code-pedimento-regimens/', data),
/**
* Actualiza un code pedimento regimen existente
*/
update: (id: number, data: UpdateCodePedimentoRegimenData) =>
// CORRECTO: Slash después del ID
api.put<CodePedimentoRegimen>(`/v1/public/refrence_data/code-pedimento-regimens/${id}/`, data),
api.put<CodePedimentoRegimen>(`/v1/public/reference_data/code-pedimento-regimens/${id}/`, data),
/**
* Elimina un code pedimento regimen
*/
delete: (id: number) =>
// CORRECTO: Slash después del ID
api.delete(`/v1/public/refrence_data/code-pedimento-regimens/${id}/`)
api.delete(`/v1/public/reference_data/code-pedimento-regimens/${id}/`)
};

View File

@@ -38,7 +38,7 @@ export const containersApi = {
list: (page = 1, pageSize = 50) =>
api.get<ContainerListResponse>(
// CORRECTO: Slash antes del ?
`/v1/public/refrence_data/containers/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}`
),
/**
@@ -55,7 +55,7 @@ export const containersApi = {
*/
create: (data: CreateContainerData) =>
// CORREGIDO: Se añadió el slash '/' al final para evitar redirección en POST
api.post<Container>('/v1/public/refrence_data/containers/', data),
api.post<Container>('/v1/public/reference_data/containers/', data),
/**
* Actualiza un container existente
@@ -64,7 +64,7 @@ export const containersApi = {
*/
update: (key: number, data: UpdateContainerData) =>
// CORRECTO: Slash final después de la key
api.put<Container>(`/v1/public/refrence_data/containers/${key}/`, data),
api.put<Container>(`/v1/public/reference_data/containers/${key}/`, data),
/**
* Elimina un container
@@ -72,5 +72,5 @@ export const containersApi = {
*/
delete: (key: number) =>
// CORRECTO: Slash final después de la key
api.delete(`/v1/public/refrence_data/containers/${key}/`)
api.delete(`/v1/public/reference_data/containers/${key}/`)
};

View File

@@ -46,7 +46,7 @@ export const countriesApi = {
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/refrence_data/countries/?page=${page}&page_size=${pageSize}`;
let url = `/v1/public/reference_data/countries/?page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -59,7 +59,7 @@ export const countriesApi = {
*/
get: (m3_key: string) =>
// CORREGIDO: Añadido '/' al final
api.get<Country>(`/v1/public/refrence_data/countries/${m3_key}/`),
api.get<Country>(`/v1/public/reference_data/countries/${m3_key}/`),
/**
* Crea un nuevo país
@@ -67,7 +67,7 @@ export const countriesApi = {
*/
create: (data: CreateCountryData) =>
// CORREGIDO: Añadido '/' al final
api.post<Country>('/v1/public/refrence_data/countries/', data),
api.post<Country>('/v1/public/reference_data/countries/', data),
/**
* Actualiza un país existente
@@ -76,7 +76,7 @@ export const countriesApi = {
*/
update: (m3_key: string, data: UpdateCountryData) =>
// CORREGIDO: Añadido '/' después de la clave
api.put<Country>(`/v1/public/refrence_data/countries/${m3_key}/`, data),
api.put<Country>(`/v1/public/reference_data/countries/${m3_key}/`, data),
/**
* Elimina un país
@@ -84,5 +84,5 @@ export const countriesApi = {
*/
delete: (m3_key: string) =>
// CORREGIDO: Añadido '/' después de la clave
api.delete(`/v1/public/refrence_data/countries/${m3_key}/`)
api.delete(`/v1/public/reference_data/countries/${m3_key}/`)
};

View File

@@ -41,7 +41,7 @@ export const currencyTypesApi = {
list: (page = 1, pageSize = 50) =>
api.get<CurrencyTypeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/currency-types/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}`
),
/**
@@ -50,7 +50,7 @@ export const currencyTypesApi = {
*/
get: (code: string) =>
// CORREGIDO: Añadido '/' al final
api.get<CurrencyType>(`/v1/public/refrence_data/currency-types/${code}/`),
api.get<CurrencyType>(`/v1/public/reference_data/currency-types/${code}/`),
/**
* Crea un nuevo tipo de moneda
@@ -58,7 +58,7 @@ export const currencyTypesApi = {
*/
create: (data: CreateCurrencyTypeData) =>
// CORREGIDO: Añadido '/' al final
api.post<CurrencyType>('/v1/public/refrence_data/currency-types/', data),
api.post<CurrencyType>('/v1/public/reference_data/currency-types/', data),
/**
* Actualiza un tipo de moneda existente
@@ -67,7 +67,7 @@ export const currencyTypesApi = {
*/
update: (code: string, data: UpdateCurrencyTypeData) =>
// CORREGIDO: Añadido '/' después del código
api.put<CurrencyType>(`/v1/public/refrence_data/currency-types/${code}/`, data),
api.put<CurrencyType>(`/v1/public/reference_data/currency-types/${code}/`, data),
/**
* Elimina un tipo de moneda
@@ -75,5 +75,5 @@ export const currencyTypesApi = {
*/
delete: (code: string) =>
// CORREGIDO: Añadido '/' después del código
api.delete(`/v1/public/refrence_data/currency-types/${code}/`)
api.delete(`/v1/public/reference_data/currency-types/${code}/`)
};

View File

@@ -38,7 +38,7 @@ export const customsSectionsApi = {
list: (page = 1, pageSize = 50) =>
api.get<CustomsSectionListResponse>(
// CORREGIDO: Añadido '/' antes de los parámetros
`/v1/public/refrence_data/customs-sections/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}`
),
/**
@@ -47,7 +47,7 @@ export const customsSectionsApi = {
*/
get: (customs_code: string) =>
// CORREGIDO: Añadido '/' final
api.get<CustomsSection>(`/v1/public/refrence_data/customs-sections/${customs_code}/`),
api.get<CustomsSection>(`/v1/public/reference_data/customs-sections/${customs_code}/`),
/**
* Crea una nueva sección aduanera
@@ -55,7 +55,7 @@ export const customsSectionsApi = {
*/
create: (data: CreateCustomsSectionData) =>
// CORREGIDO: Añadido '/' final
api.post<CustomsSection>('/v1/public/refrence_data/customs-sections/', data),
api.post<CustomsSection>('/v1/public/reference_data/customs-sections/', data),
/**
* Actualiza una sección aduanera existente
@@ -64,7 +64,7 @@ export const customsSectionsApi = {
*/
update: (customs_code: string, data: UpdateCustomsSectionData) =>
// CORREGIDO: Añadido '/' después del código
api.put<CustomsSection>(`/v1/public/refrence_data/customs-sections/${customs_code}/`, data),
api.put<CustomsSection>(`/v1/public/reference_data/customs-sections/${customs_code}/`, data),
/**
* Elimina una sección aduanera
@@ -72,5 +72,5 @@ export const customsSectionsApi = {
*/
delete: (customs_code: string) =>
// CORREGIDO: Añadido '/' después del código
api.delete(`/v1/public/refrence_data/customs-sections/${customs_code}/`)
api.delete(`/v1/public/reference_data/customs-sections/${customs_code}/`)
};

View File

@@ -41,7 +41,7 @@ export const customsWarehousesApi = {
list: (page = 1, pageSize = 50) =>
api.get<CustomsWarehouseListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/customs-warehouses/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}`
),
/**
@@ -51,7 +51,7 @@ export const customsWarehousesApi = {
*/
get: (key: string, customs: string) =>
// CORREGIDO: Añadido '/' al final de la ruta compuesta
api.get<CustomsWarehouse>(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}/`),
api.get<CustomsWarehouse>(`/v1/public/reference_data/customs-warehouses/${key}/${customs}/`),
/**
* Crea un nuevo recinto fiscalizado
@@ -59,7 +59,7 @@ export const customsWarehousesApi = {
*/
create: (data: CreateCustomsWarehouseData) =>
// CORREGIDO: Añadido '/' al final
api.post<CustomsWarehouse>('/v1/public/refrence_data/customs-warehouses/', data),
api.post<CustomsWarehouse>('/v1/public/reference_data/customs-warehouses/', data),
/**
* Actualiza un recinto fiscalizado existente
@@ -69,7 +69,7 @@ export const customsWarehousesApi = {
*/
update: (key: string, customs: string, data: UpdateCustomsWarehouseData) =>
// CORREGIDO: Añadido '/' al final de la ruta compuesta
api.put<CustomsWarehouse>(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}/`, data),
api.put<CustomsWarehouse>(`/v1/public/reference_data/customs-warehouses/${key}/${customs}/`, data),
/**
* Elimina un recinto fiscalizado
@@ -78,5 +78,5 @@ export const customsWarehousesApi = {
*/
delete: (key: string, customs: string) =>
// CORREGIDO: Añadido '/' al final de la ruta compuesta
api.delete(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}/`)
api.delete(`/v1/public/reference_data/customs-warehouses/${key}/${customs}/`)
};

View File

@@ -41,7 +41,7 @@ export const incotermsApi = {
list: (page = 1, pageSize = 50) =>
api.get<IncotermListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/incoterms/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`
),
/**
@@ -50,7 +50,7 @@ export const incotermsApi = {
*/
get: (code: string) =>
// CORREGIDO: Añadido '/' final
api.get<Incoterm>(`/v1/public/refrence_data/incoterms/${code}/`),
api.get<Incoterm>(`/v1/public/reference_data/incoterms/${code}/`),
/**
* Crea un nuevo incoterm
@@ -58,7 +58,7 @@ export const incotermsApi = {
*/
create: (data: CreateIncotermData) =>
// CORREGIDO: Añadido '/' final
api.post<Incoterm>('/v1/public/refrence_data/incoterms/', data),
api.post<Incoterm>('/v1/public/reference_data/incoterms/', data),
/**
* Actualiza un incoterm existente
@@ -67,7 +67,7 @@ export const incotermsApi = {
*/
update: (code: string, data: UpdateIncotermData) =>
// CORREGIDO: Añadido '/' final después del código
api.put<Incoterm>(`/v1/public/refrence_data/incoterms/${code}/`, data),
api.put<Incoterm>(`/v1/public/reference_data/incoterms/${code}/`, data),
/**
* Elimina un incoterm
@@ -75,5 +75,5 @@ export const incotermsApi = {
*/
delete: (code: string) =>
// CORREGIDO: Añadido '/' final después del código
api.delete(`/v1/public/refrence_data/incoterms/${code}/`)
api.delete(`/v1/public/reference_data/incoterms/${code}/`)
};

View File

@@ -53,7 +53,7 @@ export const invoiceTypesApi = {
}
return api.get<InvoiceTypeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/invoice-types/?${params.toString()}`
`/v1/public/reference_data/invoice-types/?${params.toString()}`
);
},
@@ -63,7 +63,7 @@ export const invoiceTypesApi = {
*/
get: (key: string) =>
// CORREGIDO: Añadido '/' final
api.get<InvoiceType>(`/v1/public/refrence_data/invoice-types/${key}/`),
api.get<InvoiceType>(`/v1/public/reference_data/invoice-types/${key}/`),
/**
* Crea un nuevo tipo de factura
@@ -71,7 +71,7 @@ export const invoiceTypesApi = {
*/
create: (data: CreateInvoiceTypeData) =>
// CORREGIDO: Añadido '/' final
api.post<InvoiceType>('/v1/public/refrence_data/invoice-types/', data),
api.post<InvoiceType>('/v1/public/reference_data/invoice-types/', data),
/**
* Actualiza un tipo de factura existente
@@ -80,7 +80,7 @@ export const invoiceTypesApi = {
*/
update: (key: string, data: UpdateInvoiceTypeData) =>
// CORREGIDO: Añadido '/' después de la key
api.put<InvoiceType>(`/v1/public/refrence_data/invoice-types/${key}/`, data),
api.put<InvoiceType>(`/v1/public/reference_data/invoice-types/${key}/`, data),
/**
* Elimina un tipo de factura
@@ -88,5 +88,5 @@ export const invoiceTypesApi = {
*/
delete: (key: string) =>
// CORREGIDO: Añadido '/' después de la key
api.delete(`/v1/public/refrence_data/invoice-types/${key}/`)
api.delete(`/v1/public/reference_data/invoice-types/${key}/`)
};

View File

@@ -49,7 +49,7 @@ export const materialTypesApi = {
}
return api.get<MaterialTypeListResponse>(
// CORRECTO: Ya tiene el '/' antes del '?'
`/v1/public/refrence_data/material-types/?${params.toString()}`
`/v1/public/reference_data/material-types/?${params.toString()}`
);
},
@@ -59,7 +59,7 @@ export const materialTypesApi = {
*/
get: (key: string) =>
// CORRECTO: Ya tiene el '/' al final
api.get<MaterialType>(`/v1/public/refrence_data/material-types/${key}/`),
api.get<MaterialType>(`/v1/public/reference_data/material-types/${key}/`),
/**
* Crea un nuevo tipo de material
@@ -67,7 +67,7 @@ export const materialTypesApi = {
*/
create: (data: CreateMaterialTypeData) =>
// CORRECTO: Ya tiene el '/' al final
api.post<MaterialType>('/v1/public/refrence_data/material-types/', data),
api.post<MaterialType>('/v1/public/reference_data/material-types/', data),
/**
* Actualiza un tipo de material existente
@@ -76,7 +76,7 @@ export const materialTypesApi = {
*/
update: (key: string, data: UpdateMaterialTypeData) =>
// CORRECTO: Ya tiene el '/' al final
api.put<MaterialType>(`/v1/public/refrence_data/material-types/${key}/`, data),
api.put<MaterialType>(`/v1/public/reference_data/material-types/${key}/`, data),
/**
* Elimina un tipo de material
@@ -84,5 +84,5 @@ export const materialTypesApi = {
*/
delete: (key: string) =>
// CORRECTO: Ya tiene el '/' al final
api.delete(`/v1/public/refrence_data/material-types/${key}/`)
api.delete(`/v1/public/reference_data/material-types/${key}/`)
};

View File

@@ -38,7 +38,7 @@ export const paymentMethodsApi = {
list: (page = 1, pageSize = 50) =>
api.get<PaymentMethodListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/payment-methods/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}`
),
/**
@@ -47,7 +47,7 @@ export const paymentMethodsApi = {
*/
get: (key: string) =>
// CORREGIDO: Añadido '/' final
api.get<PaymentMethod>(`/v1/public/refrence_data/payment-methods/${key}/`),
api.get<PaymentMethod>(`/v1/public/reference_data/payment-methods/${key}/`),
/**
* Crea un nuevo método de pago
@@ -55,7 +55,7 @@ export const paymentMethodsApi = {
*/
create: (data: CreatePaymentMethodData) =>
// CORREGIDO: Añadido '/' final
api.post<PaymentMethod>('/v1/public/refrence_data/payment-methods/', data),
api.post<PaymentMethod>('/v1/public/reference_data/payment-methods/', data),
/**
* Actualiza un método de pago existente
@@ -64,7 +64,7 @@ export const paymentMethodsApi = {
*/
update: (key: string, data: UpdatePaymentMethodData) =>
// CORREGIDO: Añadido '/' después de la key
api.put<PaymentMethod>(`/v1/public/refrence_data/payment-methods/${key}/`, data),
api.put<PaymentMethod>(`/v1/public/reference_data/payment-methods/${key}/`, data),
/**
* Elimina un método de pago
@@ -72,5 +72,5 @@ export const paymentMethodsApi = {
*/
delete: (key: string) =>
// CORREGIDO: Añadido '/' después de la key
api.delete(`/v1/public/refrence_data/payment-methods/${key}/`)
api.delete(`/v1/public/reference_data/payment-methods/${key}/`)
};

View File

@@ -38,7 +38,7 @@ export const pedimentoCodesApi = {
list: (page = 1, pageSize = 50) =>
api.get<PedimentoCodeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/pedimento-codes/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}`
),
/**
@@ -47,7 +47,7 @@ export const pedimentoCodesApi = {
*/
get: (code: string) =>
// CORREGIDO: Añadido '/' final
api.get<PedimentoCode>(`/v1/public/refrence_data/pedimento-codes/${code}/`),
api.get<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/${code}/`),
/**
* Crea una nueva clave de pedimento
@@ -55,7 +55,7 @@ export const pedimentoCodesApi = {
*/
create: (data: CreatePedimentoCodeData) =>
// CORREGIDO: Añadido '/' final
api.post<PedimentoCode>('/v1/public/refrence_data/pedimento-codes/', data),
api.post<PedimentoCode>('/v1/public/reference_data/pedimento-codes/', data),
/**
* Actualiza una clave de pedimento existente
@@ -64,7 +64,7 @@ export const pedimentoCodesApi = {
*/
update: (code: string, data: UpdatePedimentoCodeData) =>
// CORREGIDO: Añadido '/' después del código
api.put<PedimentoCode>(`/v1/public/refrence_data/pedimento-codes/${code}/`, data),
api.put<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/${code}/`, data),
/**
* Elimina una clave de pedimento
@@ -72,5 +72,5 @@ export const pedimentoCodesApi = {
*/
delete: (code: string) =>
// CORREGIDO: Añadido '/' después del código
api.delete(`/v1/public/refrence_data/pedimento-codes/${code}/`)
api.delete(`/v1/public/reference_data/pedimento-codes/${code}/`)
};

View File

@@ -38,7 +38,7 @@ export const pedimentoRegimensApi = {
list: (page = 1, pageSize = 50) =>
api.get<PedimentoRegimenListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/pedimento-regimens/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}`
),
/**
@@ -47,7 +47,7 @@ export const pedimentoRegimensApi = {
*/
get: (code: string) =>
// CORREGIDO: Añadido '/' final
api.get<PedimentoRegimen>(`/v1/public/refrence_data/pedimento-regimens/${code}/`),
api.get<PedimentoRegimen>(`/v1/public/reference_data/pedimento-regimens/${code}/`),
/**
* Crea un nuevo régimen de pedimento
@@ -55,7 +55,7 @@ export const pedimentoRegimensApi = {
*/
create: (data: CreatePedimentoRegimenData) =>
// CORREGIDO: Añadido '/' final
api.post<PedimentoRegimen>('/v1/public/refrence_data/pedimento-regimens/', data),
api.post<PedimentoRegimen>('/v1/public/reference_data/pedimento-regimens/', data),
/**
* Actualiza un régimen de pedimento existente
@@ -64,7 +64,7 @@ export const pedimentoRegimensApi = {
*/
update: (code: string, data: UpdatePedimentoRegimenData) =>
// CORREGIDO: Añadido '/' final después del código
api.put<PedimentoRegimen>(`/v1/public/refrence_data/pedimento-regimens/${code}/`, data),
api.put<PedimentoRegimen>(`/v1/public/reference_data/pedimento-regimens/${code}/`, data),
/**
* Elimina un régimen de pedimento
@@ -72,5 +72,5 @@ export const pedimentoRegimensApi = {
*/
delete: (code: string) =>
// CORREGIDO: Añadido '/' final después del código
api.delete(`/v1/public/refrence_data/pedimento-regimens/${code}/`)
api.delete(`/v1/public/reference_data/pedimento-regimens/${code}/`)
};

View File

@@ -41,7 +41,7 @@ export const sectorsApi = {
list: (page = 1, pageSize = 50) =>
api.get<SectorListResponse>(
// CORREGIDO: Slash antes del '?'
`/v1/public/refrence_data/sectors/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/sectors/?page=${page}&page_size=${pageSize}`
),
/**
@@ -50,7 +50,7 @@ export const sectorsApi = {
*/
get: (key: string) =>
// CORREGIDO: Slash final
api.get<Sector>(`/v1/public/refrence_data/sectors/${key}/`),
api.get<Sector>(`/v1/public/reference_data/sectors/${key}/`),
/**
* Crea un nuevo sector
@@ -58,7 +58,7 @@ export const sectorsApi = {
*/
create: (data: CreateSectorData) =>
// CORREGIDO: Slash final
api.post<Sector>('/v1/public/refrence_data/sectors/', data),
api.post<Sector>('/v1/public/reference_data/sectors/', data),
/**
* Actualiza un sector existente
@@ -67,7 +67,7 @@ export const sectorsApi = {
*/
update: (key: string, data: UpdateSectorData) =>
// CORREGIDO: Slash final después de la variable
api.put<Sector>(`/v1/public/refrence_data/sectors/${key}/`, data),
api.put<Sector>(`/v1/public/reference_data/sectors/${key}/`, data),
/**
* Elimina un sector
@@ -75,5 +75,5 @@ export const sectorsApi = {
*/
delete: (key: string) =>
// CORREGIDO: Slash final después de la variable
api.delete(`/v1/public/refrence_data/sectors/${key}/`)
api.delete(`/v1/public/reference_data/sectors/${key}/`)
};

View File

@@ -44,7 +44,7 @@ export const statesApi = {
list: (page = 1, pageSize = 50) =>
api.get<StateListResponse>(
// CORREGIDO: Añadido '/' antes de '?'
`/v1/public/refrence_data/states/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}`
),
/**
@@ -53,7 +53,7 @@ export const statesApi = {
*/
get: (m3Key: string) =>
// CORREGIDO: Añadido '/' al final
api.get<State>(`/v1/public/refrence_data/states/${m3Key}/`),
api.get<State>(`/v1/public/reference_data/states/${m3Key}/`),
/**
* Crea un nuevo estado
@@ -61,7 +61,7 @@ export const statesApi = {
*/
create: (data: CreateStateData) =>
// CORREGIDO: Añadido '/' al final
api.post<State>('/v1/public/refrence_data/states/', data),
api.post<State>('/v1/public/reference_data/states/', data),
/**
* Actualiza un estado existente
@@ -70,7 +70,7 @@ export const statesApi = {
*/
update: (m3Key: string, data: UpdateStateData) =>
// CORREGIDO: Añadido '/' después de la variable
api.put<State>(`/v1/public/refrence_data/states/${m3Key}/`, data),
api.put<State>(`/v1/public/reference_data/states/${m3Key}/`, data),
/**
* Elimina un estado
@@ -78,5 +78,5 @@ export const statesApi = {
*/
delete: (m3Key: string) =>
// CORREGIDO: Añadido '/' después de la variable
api.delete(`/v1/public/refrence_data/states/${m3Key}/`)
api.delete(`/v1/public/reference_data/states/${m3Key}/`)
};

View File

@@ -5,25 +5,25 @@
import { api } from '$lib/api';
export interface TransportMode {
key: string;
name: string;
key: string;
name: string;
}
export interface TransportModeListResponse {
items: TransportMode[];
total: number;
page: number;
page_size: number;
items: TransportMode[];
total: number;
page: number;
page_size: number;
}
export interface CreateTransportModeData {
key: string;
name: string;
key: string;
name: string;
}
export interface UpdateTransportModeData {
key?: string;
name?: string;
key?: string;
name?: string;
}
/**
@@ -37,7 +37,6 @@ export const transportModesApi = {
*/
list: (page = 1, pageSize = 50) =>
api.get<TransportModeListResponse>(
// CORREGIDO: Slash antes del ?
`/v1/public/refrence_data/transport-modes/?page=${page}&page_size=${pageSize}`
),
@@ -45,8 +44,7 @@ export const transportModesApi = {
* Obtiene un modo de transporte por key
* @param key - Clave del modo de transporte
*/
get: (key: string) =>
// CORREGIDO: Slash final después de la key
get: (key: string) =>
api.get<TransportMode>(`/v1/public/refrence_data/transport-modes/${key}/`),
/**
@@ -54,7 +52,6 @@ export const transportModesApi = {
* @param data - Datos del modo de transporte a crear
*/
create: (data: CreateTransportModeData) =>
// CORREGIDO: Slash final en la ruta base
api.post<TransportMode>('/v1/public/refrence_data/transport-modes/', data),
/**
@@ -63,14 +60,12 @@ export const transportModesApi = {
* @param data - Datos a actualizar
*/
update: (key: string, data: UpdateTransportModeData) =>
// CORREGIDO: Slash final después de la key
api.put<TransportMode>(`/v1/public/refrence_data/transport-modes/${key}/`, data),
/**
* Elimina un modo de transporte
* @param key - Clave del modo de transporte a eliminar
*/
delete: (key: string) =>
// CORREGIDO: Slash final después de la key
delete: (key: string) =>
api.delete(`/v1/public/refrence_data/transport-modes/${key}/`)
};

View File

@@ -5,25 +5,25 @@
import { api } from '$lib/api';
export interface TransportType {
transport_code: string;
description: string;
transport_code: string;
description: string;
}
export interface TransportTypeListResponse {
items: TransportType[];
total: number;
page: number;
page_size: number;
items: TransportType[];
total: number;
page: number;
page_size: number;
}
export interface CreateTransportTypeData {
transport_code: string;
description: string;
transport_code: string;
description: string;
}
export interface UpdateTransportTypeData {
transport_code?: string;
description?: string;
transport_code?: string;
description?: string;
}
/**
@@ -35,35 +35,30 @@ export const transportTypesApi = {
*/
list: (page = 1, pageSize = 50) =>
api.get<TransportTypeListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/transport-types/?page=${page}&page_size=${pageSize}`
),
/**
* Obtiene un tipo de transporte por transport_code
*/
get: (transportCode: string) =>
// CORREGIDO: Añadido '/' al final
get: (transportCode: string) =>
api.get<TransportType>(`/v1/public/refrence_data/transport-types/${transportCode}/`),
/**
* Crea un nuevo tipo de transporte
*/
create: (data: CreateTransportTypeData) =>
// CORREGIDO: Añadido '/' al final
api.post<TransportType>('/v1/public/refrence_data/transport-types/', data),
/**
* Actualiza un tipo de transporte existente
*/
update: (transportCode: string, data: UpdateTransportTypeData) =>
// CORREGIDO: Añadido '/' después de la variable
api.put<TransportType>(`/v1/public/refrence_data/transport-types/${transportCode}/`, data),
/**
* Elimina un tipo de transporte
*/
delete: (transportCode: string) =>
// CORREGIDO: Añadido '/' después de la variable
delete: (transportCode: string) =>
api.delete(`/v1/public/refrence_data/transport-types/${transportCode}/`)
};

View File

@@ -38,7 +38,7 @@ export const valuationMethodsApi = {
list: (page = 1, pageSize = 50) =>
api.get<ValuationMethodListResponse>(
// CORREGIDO: Añadido '/' antes del '?'
`/v1/public/refrence_data/valuation-methods/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}`
),
/**
@@ -47,7 +47,7 @@ export const valuationMethodsApi = {
*/
get: (key: string) =>
// CORREGIDO: Añadido '/' final
api.get<ValuationMethod>(`/v1/public/refrence_data/valuation-methods/${key}/`),
api.get<ValuationMethod>(`/v1/public/reference_data/valuation-methods/${key}/`),
/**
* Crea un nuevo método de valoración
@@ -55,7 +55,7 @@ export const valuationMethodsApi = {
*/
create: (data: CreateValuationMethodData) =>
// CORREGIDO: Añadido '/' final
api.post<ValuationMethod>('/v1/public/refrence_data/valuation-methods/', data),
api.post<ValuationMethod>('/v1/public/reference_data/valuation-methods/', data),
/**
* Actualiza un método de valoración existente
@@ -64,7 +64,7 @@ export const valuationMethodsApi = {
*/
update: (key: string, data: UpdateValuationMethodData) =>
// CORREGIDO: Añadido '/' después de la variable key
api.put<ValuationMethod>(`/v1/public/refrence_data/valuation-methods/${key}/`, data),
api.put<ValuationMethod>(`/v1/public/reference_data/valuation-methods/${key}/`, data),
/**
* Elimina un método de valoración
@@ -72,5 +72,5 @@ export const valuationMethodsApi = {
*/
delete: (key: string) =>
// CORREGIDO: Añadido '/' después de la variable key
api.delete(`/v1/public/refrence_data/valuation-methods/${key}/`)
api.delete(`/v1/public/reference_data/valuation-methods/${key}/`)
};

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Construction } from 'lucide-svelte';
import { fade, fly } from 'svelte/transition';
import { onMount } from 'svelte';
let visible = false;
onMount(() => {
visible = true;
});
</script>
<div
class="flex h-[calc(100vh-200px)] flex-col items-center justify-center overflow-hidden p-8 text-center"
>
{#if visible}
<div
class="relative mb-8 rounded-full bg-blue-50 p-8 dark:bg-blue-900/20"
in:fly={{ y: -50, duration: 1000, delay: 200 }}
>
<div class="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-20"></div>
<Construction
class="relative z-10 h-16 w-16 animate-bounce text-blue-500 dark:text-blue-400"
/>
</div>
<h1
class="mb-4 text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
in:fade={{ duration: 1000, delay: 500 }}
>
En Construcción
</h1>
<p
class="max-w-md text-xl leading-relaxed text-gray-500 dark:text-gray-400"
in:fade={{ duration: 1000, delay: 800 }}
>
MODULO AUN NO DISPONIBLE!
</p>
<div class="mt-10" in:fly={{ y: 20, duration: 1000, delay: 1100 }}>
<a
href="/dashboard"
class="group inline-flex items-center justify-center rounded-lg bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-lg transition-all hover:scale-105 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none active:scale-95"
>
<span>Volver al Inicio</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</a>
</div>
{/if}
</div>
<style>
:global(.animate-bounce) {
animation: bounce 2s infinite;
}
@keyframes bounce {
0%,
100% {
transform: translateY(-5%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: translateY(0);
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
</style>

View File

@@ -0,0 +1,100 @@
import { renderSnippet } from "$lib/components/ui/data-table/index.js";
import { createRawSnippet } from "svelte";
import type { ColumnDef } from "@tanstack/table-core";
import type { Manifest } from "$lib/api/dashboard/a76/manifests";
export function createColumns(): ColumnDef<Manifest>[] {
return [
{
accessorKey: "manifest_number",
header: "Manifesto",
cell: ({ row }) => {
const manifestSnippet = createRawSnippet<[{ value: string | null }]>((getVal) => {
const { value } = getVal();
return {
render: () => `<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold">${value || '-'}</code>`
};
});
return renderSnippet(manifestSnippet, { value: row.original.manifest_number ?? null });
}
},
{
accessorKey: "carrier_code",
header: "Transportista",
cell: ({ row }) => {
const carrierSnippet = createRawSnippet<[{ value: string | null }]>((getVal) => {
const { value } = getVal();
return {
render: () => `<div class="max-w-[120px] truncate text-xs uppercase font-medium">${value || '-'}</div>`
};
});
return renderSnippet(carrierSnippet, { value: row.original.carrier_code ?? null });
}
},
{
accessorKey: "person_in_charge",
header: "Persona a cargo",
cell: ({ row }) => {
return row.original.person_in_charge || "-";
}
},
{
accessorKey: "foreign_exit_port",
header: "PtoSalMex",
cell: ({ row }) => {
return row.original.foreign_exit_port || "-";
}
},
{
accessorKey: "destination_port",
header: "Puerto Destino",
cell: ({ row }) => {
return row.original.destination_port || "-";
}
},
{
accessorKey: "entry_port",
header: "Puerto arribo",
cell: ({ row }) => {
return row.original.entry_port || "-";
}
},
{
accessorKey: "entry_date",
header: "Fecha entrada",
cell: ({ row }) => {
const dateSnippet = createRawSnippet<[{ value: number | null }]>((getVal) => {
const { value } = getVal();
let formattedDate = '-';
if (value) {
const dateStr = value.toString();
if (dateStr.length === 8) {
formattedDate = `${dateStr.substring(6, 8)}/${dateStr.substring(4, 6)}/${dateStr.substring(0, 4)}`;
}
}
return {
render: () => `<div class="text-xs text-muted-foreground">${formattedDate}</div>`
};
});
return renderSnippet(dateSnippet, { value: row.original.entry_date ?? null });
}
},
{
accessorKey: "status",
header: "Estatus",
cell: ({ row }) => {
const statusSnippet = createRawSnippet<[{ status: string | null }]>((getS) => {
const { status } = getS();
let colorClass = "bg-muted text-muted-foreground";
if (status === 'RECIBIDO') colorClass = "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400";
if (status === 'PENDIENTE') colorClass = "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400";
return {
render: () => `<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase ${colorClass}">${status || 'S.E.'}</span>`
};
});
return renderSnippet(statusSnippet, { status: row.original.status ?? null });
}
}
];
}

View File

@@ -0,0 +1,171 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Separator } from '$lib/components/ui/separator';
import { Textarea } from '$lib/components/ui/textarea';
import { Loader2 } from 'lucide-svelte';
import { manifestApi, type ManifestCreate } from '$lib/api/dashboard/a76/manifests';
import { toast } from 'svelte-sonner';
import { companyStore } from '$lib/stores/company.svelte';
let { open = $bindable(false), onSuccess }: { open: boolean; onSuccess?: () => void } = $props();
let loading = $state(false);
let formData = $state<ManifestCreate>({
manifest_number: '',
carrier_code: '',
entry_date: undefined,
status: 'PENDIENTE',
description: '',
broker_code: '',
person_in_charge: '',
consigned_to: '',
sent_by: '',
foreign_exit_port: '',
destination_port: '',
entry_port: '',
net_weight: 0,
gross_weight: 0,
total_value: 0
});
async function handleSubmit() {
if (!companyStore.activeCompany) return;
try {
loading = true;
if (!formData.manifest_number) {
toast.error('El número de manifiesto es obligatorio');
return;
}
const response = await manifestApi.create(companyStore.activeCompany.id.toString(), formData);
if (response.error) {
toast.error(response.error);
return;
}
toast.success('Manifiesto creado correctamente');
open = false;
onSuccess?.();
} catch (error) {
console.error(error);
toast.error('Error al crear el manifiesto');
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Nuevo Manifiesto</Dialog.Title>
<Dialog.Description>
Ingresa los datos generales del nuevo manifiesto de exportación.
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-6 py-4">
<div class="space-y-4">
<h4 class="text-xs font-bold tracking-wider text-muted-foreground uppercase">
Información General
</h4>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="manifest_number">Número de Manifiesto *</Label>
<Input
id="manifest_number"
bind:value={formData.manifest_number}
placeholder="Ej. MAN-2024-001"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="entry_date">Fecha de Entrada (YYYYMMDD)</Label>
<Input
id="entry_date"
type="number"
bind:value={formData.entry_date}
placeholder="YYYYMMDD"
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="carrier_code">Código Transportista</Label>
<Input
id="carrier_code"
bind:value={formData.carrier_code}
placeholder="Ej. TR123"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="broker_code">Código Broker</Label>
<Input
id="broker_code"
bind:value={formData.broker_code}
placeholder="Ej. BK456"
disabled={loading}
/>
</div>
</div>
</div>
<Separator />
<div class="space-y-4">
<h4 class="text-xs font-bold tracking-wider text-muted-foreground uppercase">Logística</h4>
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="entry_port">Puerto Arribo</Label>
<Input id="entry_port" bind:value={formData.entry_port} disabled={loading} />
</div>
<div class="space-y-2">
<Label for="foreign_exit_port">Puerto Salida (Ext)</Label>
<Input
id="foreign_exit_port"
bind:value={formData.foreign_exit_port}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="destination_port">Puerto Destino</Label>
<Input
id="destination_port"
bind:value={formData.destination_port}
disabled={loading}
/>
</div>
</div>
</div>
<Separator />
<div class="space-y-2">
<Label for="description">Descripción</Label>
<Textarea
id="description"
bind:value={formData.description}
placeholder="Descripción del contenido"
disabled={loading}
/>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)} disabled={loading}>Cancelar</Button>
<Button onclick={handleSubmit} disabled={loading}>
{#if loading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Guardando
{:else}
Crear Manifiesto
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,136 @@
<script lang="ts" generics="TData, TValue">
import { onMount } from 'svelte';
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
import * as Table from '$lib/components/ui/table/index.js';
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
loading?: boolean;
hasMore?: boolean;
loadMore?: () => void;
// Props para selección
selectedId?: number | null;
onRowClick?: (row: TData) => void;
};
let {
data,
columns,
loading = false,
hasMore = false,
loadMore,
selectedId = null,
onRowClick
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row: any) => row.id?.toString(),
state: {
get rowSelection() {
return selectedId ? { [selectedId]: true } : {};
}
},
enableRowSelection: true,
enableMultiRowSelection: false
});
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
onMount(() => {
if (!loadMore) return;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{
root: null, // Relative to viewport if scrollContainer is used as max-h div
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer.disconnect();
};
});
</script>
<div class="w-full">
<div class="max-h-[600px] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="sticky top-0 z-10 bg-background">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row
data-state={row.getIsSelected() && 'selected'}
class="cursor-pointer transition-colors {row.getIsSelected()
? 'bg-muted'
: 'hover:bg-muted/50'}"
onclick={() => onRowClick && onRowClick(row.original)}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
{#if loading}
<div class="flex items-center justify-center gap-2">
<div
class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"
></div>
<span class="text-sm text-muted-foreground">Cargando más...</span>
</div>
{:else}
<div class="text-sm text-muted-foreground">Desplázate para cargar más</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</div>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Loader2 } from 'lucide-svelte';
import { manifestApi, type Manifest } from '$lib/api/dashboard/a76/manifests';
import { toast } from 'svelte-sonner';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
manifest,
onSuccess
}: { open: boolean; manifest: Manifest | null; onSuccess?: () => void } = $props();
let loading = $state(false);
async function handleDelete() {
if (!manifest || !companyStore.activeCompany) return;
try {
loading = true;
const response = await manifestApi.delete(
companyStore.activeCompany.id.toString(),
manifest.id
);
if (response.error) {
toast.error(response.error);
return;
}
toast.success('Manifiesto eliminado correctamente');
open = false;
onSuccess?.();
} catch (error) {
console.error(error);
toast.error('Error al eliminar el manifiesto');
} finally {
loading = false;
}
}
</script>
<AlertDialog.Root bind:open>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
<AlertDialog.Description class="space-y-2">
<p>Esta acción no se puede deshacer. Se eliminará permanentemente el manifiesto:</p>
{#if manifest}
<div class="mt-2 rounded-lg bg-muted p-3">
<div class="flex items-center justify-between text-sm">
<span class="font-medium">Manifiesto:</span>
<code class="font-mono font-semibold">{manifest.manifest_number}</code>
</div>
<div class="mt-1 truncate text-xs text-muted-foreground">
{manifest.description || 'Sin descripción'}
</div>
</div>
{/if}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel disabled={loading}>Cancelar</AlertDialog.Cancel>
<AlertDialog.Action
onclick={handleDelete}
disabled={loading}
class="text-destructive-foreground bg-destructive hover:bg-destructive/90"
>
{#if loading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Eliminando...
{:else}
Eliminar
{/if}
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>

View File

@@ -0,0 +1,164 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Separator } from '$lib/components/ui/separator';
import { FileText, Truck, MapPin, Calendar, Scale, DollarSign, Tag, Info } from 'lucide-svelte';
import type { Manifest } from '$lib/api/dashboard/a76/manifests';
let { open = $bindable(false), manifest }: { open: boolean; manifest: Manifest | null } =
$props();
function formatDate(date: number | null | undefined) {
if (!date) return '-';
const dateStr = date.toString();
if (dateStr.length === 8) {
return `${dateStr.substring(6, 8)}/${dateStr.substring(4, 6)}/${dateStr.substring(0, 4)}`;
}
return date.toString();
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-h-[90vh] sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<FileText class="h-5 w-5 text-primary" />
Detalle del Manifiesto
</Dialog.Title>
<Dialog.Description>Información detallada del registro de exportación.</Dialog.Description>
</Dialog.Header>
<div class="max-h-[60vh] overflow-y-auto pr-4">
{#if manifest}
<div class="space-y-6 py-2">
<!-- Header info -->
<div class="grid grid-cols-2 gap-4 rounded-lg bg-muted/30 p-4">
<div class="space-y-1">
<span class="text-[10px] font-bold tracking-widest text-muted-foreground uppercase"
>Número</span
>
<p class="font-mono text-lg font-bold">{manifest.manifest_number}</p>
</div>
<div class="space-y-1">
<span class="text-[10px] font-bold tracking-widest text-muted-foreground uppercase"
>Estatus</span
>
<div>
<span
class="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary uppercase"
>
{manifest.status || 'PENDIENTE'}
</span>
</div>
</div>
</div>
<!-- Basic Data -->
<div class="space-y-4">
<h4 class="flex items-center gap-2 text-sm font-bold">
<Info class="h-4 w-4" /> Datos de Identificación
</h4>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 text-sm">
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Tipo Manifiesto</span>
<p class="font-medium">{manifest.manifest_type || '-'}</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Fecha Entrada</span>
<p class="flex items-center gap-1 font-medium">
<Calendar size={12} />
{formatDate(manifest.entry_date)}
</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Aduana Brooker</span>
<p class="font-medium">{manifest.broker_code || '-'}</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Transportista</span>
<p class="flex items-center gap-1 font-medium">
<Truck size={12} />
{manifest.carrier_code || '-'}
</p>
</div>
</div>
</div>
<Separator />
<!-- Logistics -->
<div class="space-y-4">
<h4 class="flex items-center gap-2 text-sm font-bold">
<MapPin class="h-4 w-4" /> Ruta y Logística
</h4>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 text-sm">
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Puerto Entrada</span>
<p class="font-medium">
{manifest.entry_port || '-'}
{manifest.entry_port_loc ? `(${manifest.entry_port_loc})` : ''}
</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Puerto Salida Extranjero</span>
<p class="font-medium">
{manifest.foreign_exit_port || '-'}
{manifest.foreign_exit_port_loc ? `(${manifest.foreign_exit_port_loc})` : ''}
</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Puerto Destino</span>
<p class="font-medium">
{manifest.destination_port || '-'}
{manifest.destination_port_loc ? `(${manifest.destination_port_loc})` : ''}
</p>
</div>
</div>
</div>
<Separator />
<!-- Values/Weights -->
<div class="space-y-4">
<h4 class="flex items-center gap-2 text-sm font-bold">
<Scale class="h-4 w-4" /> Pesos y Valores
</h4>
<div class="grid grid-cols-3 gap-4 text-sm">
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Peso Neto</span>
<p class="font-medium">{manifest.net_weight || 0} Kg</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Peso Bruto</span>
<p class="font-medium">{manifest.gross_weight || 0} Kg</p>
</div>
<div class="space-y-1">
<span class="text-xs text-muted-foreground">Valor Total</span>
<p class="flex items-center gap-0.5 font-medium">
<DollarSign size={12} />
{manifest.total_value || 0}
</p>
</div>
</div>
</div>
{#if manifest.description}
<Separator />
<div class="space-y-2 text-sm">
<h4 class="flex items-center gap-2 font-bold">
<Tag class="h-4 w-4" /> Descripción
</h4>
<p class="rounded bg-muted/20 p-3 text-muted-foreground italic">
{manifest.description}
</p>
</div>
{/if}
</div>
{/if}
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)}>Cerrar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Separator } from '$lib/components/ui/separator';
import { Textarea } from '$lib/components/ui/textarea';
import { Loader2 } from 'lucide-svelte';
import {
manifestApi,
type Manifest,
type ManifestUpdate
} from '$lib/api/dashboard/a76/manifests';
import { toast } from 'svelte-sonner';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
manifest,
onSuccess
}: { open: boolean; manifest: Manifest | null; onSuccess?: () => void } = $props();
let loading = $state(false);
let formData = $state<ManifestUpdate>({});
$effect(() => {
if (open && manifest) {
formData = {
manifest_number: manifest.manifest_number || '',
carrier_code: manifest.carrier_code || '',
entry_date: manifest.entry_date,
status: manifest.status || 'PENDIENTE',
description: manifest.description || '',
broker_code: manifest.broker_code || '',
person_in_charge: manifest.person_in_charge || '',
consigned_to: manifest.consigned_to || '',
sent_by: manifest.sent_by || '',
foreign_exit_port: manifest.foreign_exit_port || '',
destination_port: manifest.destination_port || '',
entry_port: manifest.entry_port || '',
net_weight: manifest.net_weight || 0,
gross_weight: manifest.gross_weight || 0,
total_value: manifest.total_value || 0
};
}
});
async function handleSubmit() {
if (!manifest || !companyStore.activeCompany) return;
try {
loading = true;
const response = await manifestApi.update(
companyStore.activeCompany.id.toString(),
manifest.id,
formData
);
if (response.error) {
toast.error(response.error);
return;
}
toast.success('Manifiesto actualizado correctamente');
open = false;
onSuccess?.();
} catch (error) {
console.error(error);
toast.error('Error al actualizar el manifiesto');
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Editar Manifiesto</Dialog.Title>
<Dialog.Description>
Modifica los datos del manifiesto <span class="font-mono font-semibold"
>{manifest?.manifest_number}</span
>.
</Dialog.Description>
</Dialog.Header>
{#if manifest}
<div class="grid gap-6 py-4">
<div class="space-y-4">
<h4 class="text-xs font-bold tracking-wider text-muted-foreground uppercase">
Información General
</h4>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-manifest_number">Número de Manifiesto</Label>
<Input
id="edit-manifest_number"
bind:value={formData.manifest_number}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-entry_date">Fecha de Entrada</Label>
<Input
id="edit-entry_date"
type="number"
bind:value={formData.entry_date}
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-carrier_code">Código Transportista</Label>
<Input id="edit-carrier_code" bind:value={formData.carrier_code} disabled={loading} />
</div>
<div class="space-y-2">
<Label for="edit-broker_code">Código Broker</Label>
<Input id="edit-broker_code" bind:value={formData.broker_code} disabled={loading} />
</div>
</div>
</div>
<Separator />
<div class="space-y-4">
<h4 class="text-xs font-bold tracking-wider text-muted-foreground uppercase">
Logística
</h4>
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="edit-entry_port">Puerto Arribo</Label>
<Input id="edit-entry_port" bind:value={formData.entry_port} disabled={loading} />
</div>
<div class="space-y-2">
<Label for="edit-foreign_exit_port">Puerto Salida (Ext)</Label>
<Input
id="edit-foreign_exit_port"
bind:value={formData.foreign_exit_port}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-destination_port">Puerto Destino</Label>
<Input
id="edit-destination_port"
bind:value={formData.destination_port}
disabled={loading}
/>
</div>
</div>
</div>
<Separator />
<div class="space-y-2">
<Label for="edit-description">Descripción</Label>
<Textarea id="edit-description" bind:value={formData.description} disabled={loading} />
</div>
</div>
{/if}
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)} disabled={loading}>Cancelar</Button>
<Button onclick={handleSubmit} disabled={loading}>
{#if loading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Guardando
{:else}
Guardar Cambios
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,123 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Table from '$lib/components/ui/table';
import { Badge } from '$lib/components/ui/badge';
import { Plus, Minus, Search, Loader2, Ghost } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import { fly } from 'svelte/transition';
let {
title = '',
invoices = [] as Invoice[],
loading = false,
isAdding = false, // True if this table is for "Available" invoices (to add)
readOnly = false,
onAction = (invoice: Invoice) => {}
} = $props();
let searchTerm = $state('');
// Filtro local simple
const filteredInvoices = $derived(
invoices.filter((inv) => {
if (!searchTerm) return true;
const term = searchTerm.toLowerCase();
return (
inv.invoice_number?.toLowerCase().includes(term) ||
inv.invoice_type?.toLowerCase().includes(term)
);
})
);
</script>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-muted-foreground uppercase">
{title}
<Badge variant="secondary" class="ml-2">{filteredInvoices.length}</Badge>
</h3>
<div class="relative w-48">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="h-9 pl-8" bind:value={searchTerm} />
</div>
</div>
<div class="relative h-[300px] overflow-auto rounded-md border">
<Table.Root>
<Table.Header class="sticky top-0 z-10 bg-background">
<Table.Row>
<Table.Head>Factura Expo</Table.Head>
<Table.Head>Fecha</Table.Head>
<Table.Head>P. Neto</Table.Head>
<Table.Head>P. Bruto</Table.Head>
<Table.Head>Bultos</Table.Head>
<Table.Head>Valor (USD)</Table.Head>
<Table.Head>Tipo</Table.Head>
<Table.Head class="w-[50px]"></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if loading}
<Table.Row>
<Table.Cell colspan={8} class="h-24 text-center">
<div class="flex items-center justify-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" />
<span>Cargando...</span>
</div>
</Table.Cell>
</Table.Row>
{:else if filteredInvoices.length === 0}
<Table.Row>
<Table.Cell colspan={8} class="h-32 text-center text-muted-foreground">
<div class="flex flex-col items-center justify-center gap-2">
<Ghost class="h-8 w-8 opacity-20" />
<span>No hay facturas disponibles</span>
</div>
</Table.Cell>
</Table.Row>
{:else}
{#each filteredInvoices as invoice (invoice.id)}
<tr
class="border-b transition-colors data-[state=selected]:bg-muted {invoice.is_selected
? 'bg-primary/5'
: ''} {readOnly ? '' : 'cursor-pointer hover:bg-muted/50 hover:bg-primary/10'}"
onclick={() => !readOnly && onAction(invoice)}
in:fly={{ y: 20, duration: 300 }}
out:fly={{ x: isAdding ? 20 : -20, duration: 200 }}
>
<Table.Cell class="font-mono text-xs font-medium">
<span class={invoice.is_selected ? 'font-bold text-primary' : ''}
>{invoice.invoice_number}</span
>
</Table.Cell>
<Table.Cell class="text-xs">{invoice.invoice_date}</Table.Cell>
<Table.Cell class="text-xs">{invoice.financials?.net_weight ?? '-'}</Table.Cell>
<Table.Cell class="text-xs">{invoice.financials?.gross_weight ?? '-'}</Table.Cell>
<Table.Cell class="text-xs">{invoice.financials?.bundle_count ?? '-'}</Table.Cell>
<Table.Cell class="text-xs font-medium text-green-600">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
Number(invoice.financials?.value_me ?? 0)
)}
</Table.Cell>
<Table.Cell class="text-xs"
><Badge variant="outline" class="bg-background">{invoice.invoice_type}</Badge
></Table.Cell
>
<Table.Cell>
<!-- Visual Indicator Only -->
<div class="flex items-center justify-center">
<div
class="h-4 w-4 rounded-full border border-primary {invoice.is_selected
? 'bg-primary'
: 'bg-transparent'} transition-colors"
></div>
</div>
</Table.Cell>
</tr>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
<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 { Search, Loader2, Award } from 'lucide-svelte';
import { customsBrokersApi, type CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: CustomsBroker) => void;
} = $props();
let items = $state<CustomsBroker[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
let filteredItems = $derived(
items.filter(
(i) =>
i.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.broker_key?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.license?.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadItems();
}
});
async function loadItems() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
const res = await customsBrokersApi.list(companyStore.activeCompany.id.toString());
if (res.data?.items) {
items = res.data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading customs brokers:', e);
} finally {
loading = false;
}
}
function handleSelect(item: CustomsBroker) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Agente Aduanal</Dialog.Title>
<Dialog.Description>Busca y selecciona un agente aduanal del catálogo.</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por nombre, patente o clave..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron registros.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="w-[100px] p-3 font-medium text-muted-foreground">Patente</th>
<th class="p-3 font-medium text-muted-foreground">Nombre</th>
<th class="w-[150px] p-3 font-medium text-muted-foreground">Tipo</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono text-xs font-bold text-blue-600">{item.license}</td>
<td class="p-3">
<div class="flex items-center gap-2">
<Award class="h-3 w-3 text-muted-foreground" />
{item.name || item.broker_key}
</div>
</td>
<td class="p-3 text-xs text-muted-foreground">
{item.type || 'General'}
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredItems.length} registros
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,136 @@
<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 { Search, Loader2, Anchor, MapPin } from 'lucide-svelte';
import { portsApi, type Port } from '$lib/api/dashboard/a76/general_catalogs/ports';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: Port) => void;
} = $props();
let items = $state<Port[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
// Mostramos todo, solo filtrado por búsqueda
let filteredItems = $derived(
items.filter((i) => {
const search = searchTerm.toLowerCase();
return (
!searchTerm ||
i.description?.toLowerCase().includes(search) ||
i.port_code?.toLowerCase().includes(search) ||
i.location_description?.toLowerCase().includes(search)
);
})
);
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadItems();
}
});
async function loadItems() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
// Usamos parámetros de paginación estándar para evitar errores 422
const res = await portsApi.list(companyStore.activeCompany.id, {
page: 1,
page_size: 100
});
if (res.data && res.data.items) {
items = res.data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading ports:', e);
} finally {
loading = false;
}
}
function handleSelect(item: Port) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Puerto</Dialog.Title>
<Dialog.Description>Catálogo general de puertos y aduanas.</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por descripción, clave o ubicación..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron puertos con los criterios actuales.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="w-[80px] p-3 font-medium text-muted-foreground">Clave</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
<th class="w-[150px] p-3 font-medium text-muted-foreground">Ubicación</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono font-bold text-primary">{item.port_code}</td>
<td class="p-3">
<div class="flex items-center gap-2">
<Anchor class="h-3 w-3 text-muted-foreground" />
{item.description || '-'}
</div>
</td>
<td class="p-3 transition-colors">
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<MapPin class="h-3 w-3" />
{item.location_description || item.location_code || '-'}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredItems.length} registros
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,132 @@
<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 { Search, Loader2, Truck } from 'lucide-svelte';
import { trailersApi, type Trailer } from '$lib/api/dashboard/a76/trailers';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: Trailer) => void;
} = $props();
let items = $state<Trailer[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
let filteredItems = $derived(
items.filter(
(i) =>
i.trailer_number?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.plate_number?.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadItems();
}
});
async function loadItems() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
const res = await trailersApi.list(companyStore.activeCompany.id);
const data = (res as any).data || res;
if (data && data.items) {
items = data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading trailers:', e);
} finally {
loading = false;
}
}
function handleSelect(item: Trailer) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[600px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Trailer</Dialog.Title>
<Dialog.Description>Busca y selecciona un trailer registrado.</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por número o placas..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron trailers.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="p-3 font-medium text-muted-foreground">Número de Trailer</th>
<th class="p-3 font-medium text-muted-foreground">Placas</th>
<th class="w-[100px] p-3 text-center font-medium text-muted-foreground">Estado</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono font-bold text-blue-600">{item.trailer_number}</td>
<td class="p-3 font-mono text-xs">{item.plate_number || '-'}</td>
<td class="p-3 text-center">
{#if item.is_active}
<span
class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-[10px] font-medium text-green-800"
>
Disponible
</span>
{:else}
<span
class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-[10px] font-medium text-red-800"
>
Inactivo
</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredItems.length} registros
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,115 @@
<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 { Search, Loader2 } from 'lucide-svelte';
import {
transportModesApi,
type TransportMode
} from '$lib/api/dashboard/refrence_data/transport_modes';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: TransportMode) => void;
} = $props();
let items = $state<TransportMode[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
let filteredItems = $derived(
items.filter(
(i) =>
i.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.key?.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded) {
loadItems();
}
});
async function loadItems() {
loading = true;
try {
const res = await transportModesApi.list();
if (res.data?.items) {
items = res.data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading transport modes:', e);
} finally {
loading = false;
}
}
function handleSelect(item: TransportMode) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[500px]">
<Dialog.Header>
<Dialog.Title>Modo de Transporte</Dialog.Title>
<Dialog.Description>
Selecciona la clave correspondiente al modo de transporte.
</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por clave o descripción..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron resultados.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="w-[80px] p-3 font-medium text-muted-foreground">Clave</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono font-bold">{item.key}</td>
<td class="p-3">{item.name}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,121 @@
<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 { Search, Loader2, Milestone } from 'lucide-svelte';
import {
transportTypesApi,
type TransportType
} from '$lib/api/dashboard/refrence_data/transport_types';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: TransportType) => void;
} = $props();
let items = $state<TransportType[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
let filteredItems = $derived(
items.filter(
(i) =>
i.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.transport_code?.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded) {
loadItems();
}
});
async function loadItems() {
loading = true;
try {
const res = await transportTypesApi.list();
if (res.data?.items) {
items = res.data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading transport types:', e);
} finally {
loading = false;
}
}
function handleSelect(item: TransportType) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Medio de Transporte</Dialog.Title>
<Dialog.Description>Selecciona el tipo de transporte del catálogo SAT.</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por código o descripción..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron registros.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="w-[80px] p-3 font-medium text-muted-foreground">URL Código</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono font-bold text-primary">{item.transport_code}</td>
<td class="p-3">
<div class="flex items-center gap-2">
<Milestone class="h-3 w-3 text-muted-foreground" />
{item.description}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredItems.length} registros
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,124 @@
<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 { Search, Loader2, Truck } from 'lucide-svelte';
import { transportersApi, type Transporter } from '$lib/api/dashboard/a76/transporters';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: Transporter) => void;
} = $props();
let items = $state<Transporter[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
let filteredItems = $derived(
items.filter(
(i) =>
i.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.transporter_key?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.rfc?.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadItems();
}
});
async function loadItems() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
const res = await transportersApi.list(companyStore.activeCompany.id);
const data = (res as any).data || res;
if (data && data.items) {
items = data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading transporters:', e);
} finally {
loading = false;
}
}
function handleSelect(item: Transporter) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Transportista</Dialog.Title>
<Dialog.Description>Busca y selecciona un transportista del catálogo.</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por nombre, clave o RFC..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron registros.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="w-[100px] p-3 font-medium text-muted-foreground">Clave</th>
<th class="w-[150px] p-3 font-medium text-muted-foreground">RFC</th>
<th class="p-3 font-medium text-muted-foreground">Nombre / Razón Social</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono text-xs">{item.transporter_key}</td>
<td class="p-3 font-mono text-xs">{item.rfc || '-'}</td>
<td class="p-3">
<div class="flex items-center gap-2">
<Truck class="h-3 w-3 text-muted-foreground" />
{item.name}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredItems.length} registros
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,127 @@
<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 { Search, Loader2, Car } from 'lucide-svelte';
import { vehiclesApi, type Vehicle } from '$lib/api/dashboard/a76/vehicles';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: Vehicle) => void;
} = $props();
let items = $state<Vehicle[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
let filteredItems = $derived(
items.filter(
(i) =>
i.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.vehicle_key?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.plate_number?.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.brand?.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadItems();
}
});
async function loadItems() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
const res = await vehiclesApi.list(companyStore.activeCompany.id.toString());
if (res.data?.items) {
items = res.data.items;
loaded = true;
}
} catch (e) {
console.error('Error loading vehicles:', e);
} finally {
loading = false;
}
}
function handleSelect(item: Vehicle) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Vehículo</Dialog.Title>
<Dialog.Description>Busca y selecciona un vehículo del catálogo.</Dialog.Description>
</Dialog.Header>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por placa, clave, marca o descripción..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center 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 h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron registros.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr class="border-b text-left">
<th class="w-[120px] p-3 font-medium text-muted-foreground">Clave</th>
<th class="w-[120px] p-3 font-medium text-muted-foreground">Placas</th>
<th class="p-3 font-medium text-muted-foreground">Marca / Descripción</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(item)}
>
<td class="p-3 font-mono text-xs">{item.vehicle_key}</td>
<td class="p-3 font-mono text-xs">{item.plate_number || '-'}</td>
<td class="p-3">
<div class="flex items-center gap-2">
<Car class="h-3 w-3 text-muted-foreground" />
<span>
{item.brand || ''}
{item.description || ''}
</span>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredItems.length} registros
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,156 +1,163 @@
<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 { Search, Loader2, User, Building2 } from "lucide-svelte";
import { clientsProvidersApi, type ClientProvider } from "$lib/api/dashboard/a76/clients-providers";
import { companyStore } from "$lib/stores/company.svelte";
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as Dialog from '$lib/components/ui/dialog';
import { Search, Loader2, User, Building2 } from 'lucide-svelte';
import {
clientsProvidersApi,
type ClientProvider
} from '$lib/api/dashboard/a76/clients-providers';
import { companyStore } from '$lib/stores/company.svelte';
// --- PROPS Y BINDING ---
let {
open = $bindable(false),
onSelect
}: {
open: boolean,
onSelect: (client: ClientProvider) => void
} = $props();
// --- PROPS Y BINDING ---
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (client: ClientProvider) => void;
} = $props();
// --- ESTADO LOCAL ---
let clients = $state<ClientProvider[]>([]);
let loading = $state(false);
let searchTerm = $state("");
let loaded = $state(false);
// --- ESTADO LOCAL ---
let clients = $state<ClientProvider[]>([]);
let loading = $state(false);
let searchTerm = $state('');
let loaded = $state(false);
// Filtro reactivo local
let filteredClients = $derived(
clients.filter(c =>
(c.client_or_provider === 'client' || c.client_or_provider === 'both') &&
(c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.rfc.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.id.toString().includes(searchTerm))
)
);
// Filtro reactivo local
let filteredClients = $derived(
clients.filter(
(c) =>
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.rfc.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.id.toString().includes(searchTerm)
)
);
// Efecto para cargar datos cuando se abre el modal
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadClients();
}
});
// Efecto para cargar datos cuando se abre el modal
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadClients();
}
});
async function loadClients() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
// Petición a la API - Traer todos para filtrar localmente
const res = await clientsProvidersApi.list(companyStore.activeCompany.id, 1, 1000);
async function loadClients() {
if (!companyStore.activeCompany?.id) return;
// Normalización de respuesta
const responseData = (res as any).data || res;
loading = true;
try {
// Petición a la API - Traer todos para filtrar localmente
const res = await clientsProvidersApi.list(companyStore.activeCompany.id, 1, 1000);
if (responseData && responseData.items) {
clients = responseData.items;
loaded = true;
} else {
console.warn("La API no trajo items:", responseData);
}
} catch (e) {
console.error("Error cargando clientes:", e);
} finally {
loading = false;
}
}
// Normalización de respuesta
const responseData = (res as any).data || res;
// --- FUNCIÓN DE SELECCIÓN ---
function handleSelect(client: ClientProvider) {
if (onSelect) {
onSelect(client);
}
open = false; // Cerrar el modal
}
if (responseData && responseData.items) {
clients = responseData.items;
loaded = true;
} else {
console.warn('La API no trajo items:', responseData);
}
} catch (e) {
console.error('Error cargando clientes:', e);
} finally {
loading = false;
}
}
// --- FUNCIÓN DE SELECCIÓN ---
function handleSelect(client: ClientProvider) {
if (onSelect) {
onSelect(client);
}
open = false; // Cerrar el modal
}
</script>
<Dialog.Root bind:open={open}>
<Dialog.Content class="sm:max-w-[700px] max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Cliente</Dialog.Title>
<Dialog.Description>
Busca y selecciona el cliente propietario de la parte.
</Dialog.Description>
</Dialog.Header>
<Dialog.Root bind:open>
<Dialog.Content class="flex max-h-[80vh] flex-col sm:max-w-[700px]">
<Dialog.Header>
<Dialog.Title>Seleccionar Cliente</Dialog.Title>
<Dialog.Description>
Busca y selecciona el cliente propietario de la parte.
</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 Nombre, RFC o ID..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="relative my-2 w-full">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por Nombre, RFC o ID..."
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 filteredClients.length === 0}
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
<p>No se encontraron clientes.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50 backdrop-blur-sm">
<tr class="text-left border-b">
<th class="p-3 font-medium text-muted-foreground w-[60px]">ID</th>
<th class="p-3 font-medium text-muted-foreground w-[130px]">RFC</th>
<th class="p-3 font-medium text-muted-foreground">Razón Social</th>
<th class="p-3 font-medium text-muted-foreground w-[100px] text-center">Estado</th>
</tr>
</thead>
<tbody>
{#each filteredClients as client}
<tr
class="border-b hover:bg-accent/50 transition-colors cursor-pointer"
onclick={() => handleSelect(client)}
>
<td class="p-3 font-mono text-xs">{client.id}</td>
<td class="p-3 font-mono text-xs">{client.rfc}</td>
<td class="p-3 font-medium">
<div class="flex items-center gap-2">
{#if client.client_or_provider === 'client'}
<User class="h-3 w-3 text-blue-500" />
{:else}
<Building2 class="h-3 w-3 text-purple-500" />
{/if}
{client.name}
</div>
</td>
<td class="p-3 text-center">
{#if client.is_active}
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
Activo
</span>
{:else}
<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800">
Baja
</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<div class="min-h-[300px] flex-1 overflow-y-auto rounded-md border">
{#if loading}
<div class="flex h-48 flex-col items-center justify-center gap-2 text-muted-foreground">
<Loader2 class="h-8 w-8 animate-spin text-primary" />
<p>Cargando catálogo...</p>
</div>
{:else if filteredClients.length === 0}
<div class="flex h-48 flex-col items-center justify-center text-muted-foreground">
<p>No se encontraron clientes.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50 backdrop-blur-sm">
<tr class="border-b text-left">
<th class="w-[60px] p-3 font-medium text-muted-foreground">ID</th>
<th class="w-[130px] p-3 font-medium text-muted-foreground">RFC</th>
<th class="p-3 font-medium text-muted-foreground">Razón Social</th>
<th class="w-[100px] p-3 text-center font-medium text-muted-foreground">Estado</th>
</tr>
</thead>
<tbody>
{#each filteredClients as client}
<tr
class="cursor-pointer border-b transition-colors hover:bg-accent/50"
onclick={() => handleSelect(client)}
>
<td class="p-3 font-mono text-xs">{client.id}</td>
<td class="p-3 font-mono text-xs">{client.rfc}</td>
<td class="p-3 font-medium">
<div class="flex items-center gap-2">
{#if client.client_or_provider === 'client'}
<User class="h-3 w-3 text-blue-500" />
{:else}
<Building2 class="h-3 w-3 text-purple-500" />
{/if}
{client.name}
</div>
</td>
<td class="p-3 text-center">
{#if client.is_active}
<span
class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
Activo
</span>
{:else}
<span
class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800"
>
Baja
</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="text-xs text-muted-foreground self-center mr-auto">
Mostrando {filteredClients.length} registro(s)
</div>
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<Dialog.Footer>
<div class="mr-auto self-center text-xs text-muted-foreground">
Mostrando {filteredClients.length} registro(s)
</div>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -372,6 +372,45 @@ export function getSidebarData(): SidebarData {
},
],
},
{
title: m["sidebar.export.title"](),
url: "#",
icon: ArrowUpFromLine,
items: [
{
title: m["sidebar.export.catalog"](),
url: "/dashboard/export/catalog",
},
{
title: m["sidebar.export.repair"](),
url: "/dashboard/export/repair",
},
{
title: m["sidebar.export.manifest"](),
url: "/dashboard/export/manifest",
},
{
title: m["sidebar.export.proforma"](),
url: "/dashboard/export/proforma",
},
{
title: m["sidebar.export.reports"](),
url: "/dashboard/export/reports",
},
{
title: m["sidebar.export.used_materials"](),
url: "/dashboard/export/used-materials",
},
{
title: m["sidebar.export.destruction"](),
url: "/dashboard/export/destruction",
},
{
title: m["sidebar.export.special_processes"](),
url: "/dashboard/export/special_processes",
},
],
},
{
title: m["sidebar.clients_and_providers"](),
url: "/dashboard/clients_and_providers",

View File

@@ -0,0 +1,5 @@
<script>
import Maintenance from '$lib/components/common/Maintenance.svelte';
</script>
<Maintenance />

View File

@@ -0,0 +1,5 @@
<script>
import Maintenance from '$lib/components/common/Maintenance.svelte';
</script>
<Maintenance />

View File

@@ -0,0 +1,94 @@
import { authenticatedFetch, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return {
items: [],
total: 0,
page: 1,
pageSize: 50,
error: 'No authenticated'
};
}
const search = url.searchParams.get('search') || '';
const manifest_number = url.searchParams.get('manifest_number') || '';
const start_date = url.searchParams.get('start_date') || '';
const end_date = url.searchParams.get('end_date') || '';
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('pageSize') || '50');
const skip = (page - 1) * pageSize;
const params = new URLSearchParams({
skip: skip.toString(),
limit: pageSize.toString()
});
if (search) params.append('search', search);
if (manifest_number) params.append('manifest_number', manifest_number);
if (start_date) params.append('start_date', start_date);
if (end_date) params.append('end_date', end_date);
try {
const companyIdParam = url.searchParams.get('company_id') || url.searchParams.get('companyId');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? companyIdParam
: cookieCompanyId
? cookieCompanyId
: parentData.companies?.[0]?.id?.toString();
if (!companyId) {
return {
items: [],
total: 0,
page,
pageSize,
error: null
};
}
params.append('company_id', companyId);
const response = await authenticatedFetch(
`v1/a76/manifests?${params.toString()}`,
{},
cookies,
fetch,
'/login'
);
if (!response.ok) {
return {
items: [],
total: 0,
page,
pageSize,
error: 'Error al cargar manifiestos'
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || page,
pageSize: data.page_size || pageSize,
error: null
};
} catch (error) {
console.error('Error loading manifests:', error);
return {
items: [],
total: 0,
page,
pageSize,
error: 'Error al cargar manifiestos'
};
}
};

View File

@@ -0,0 +1,267 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
manifestApi,
type Manifest,
type ManifestResponse
} from '$lib/api/dashboard/a76/manifests';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Plus, RefreshCw, FileText, Search, Package, Calendar } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { companyStore } from '$lib/stores/company.svelte';
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import DataTable from '$lib/components/dashboard/export/manifest/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/export/manifest/columns';
import DeleteDialog from '$lib/components/dashboard/export/manifest/delete-dialog.svelte';
import DetailsDialog from '$lib/components/dashboard/export/manifest/details-dialog.svelte';
let { data }: { data: any } = $props();
// --- State ---
let allItems = $state<Manifest[]>(data.items || []);
let totalItems = $state(data.total || 0);
let currentPage = $state(data.page || 1);
let pageSize = $state(50);
let loading = $state(false);
let error = $state<string | null>(data.error || null);
let selectedId = $state<number | null>(null);
const selectedItem = $derived(
selectedId ? (allItems.find((i) => i.id === selectedId) ?? null) : null
);
// Filter state
let filters = $state({
search: '',
manifest_number: ''
});
// Dialog states
let showDelete = $state(false);
let showDetails = $state(false);
const columns = createColumns();
// --- Lifecycle ---
onMount(() => {
if (browser) {
const handleCompanyChange = () => reloadData();
window.addEventListener('companyChanged', handleCompanyChange);
return () => window.removeEventListener('companyChanged', handleCompanyChange);
}
});
// --- Actions ---
async function reloadData() {
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await manifestApi.list(companyStore.activeCompany.id, {
...filters,
skip: '0',
limit: pageSize.toString()
});
if (response.error) {
toast.error(response.error);
error = response.error;
return;
}
if (response.data) {
allItems = response.data.items || [];
totalItems = response.data.total || 0;
currentPage = 1;
}
} catch (e: any) {
console.error('Error reloading manifests:', e);
toast.error('Error al cargar datos');
} finally {
loading = false;
}
}
async function loadMore() {
if (loading || allItems.length >= totalItems) return;
loading = true;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const response = await manifestApi.list(companyId, {
...filters,
skip: allItems.length.toString(),
limit: pageSize.toString()
});
if (response.error) {
toast.error(response.error);
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
totalItems = response.data.total;
currentPage++;
}
} catch (e: any) {
console.error('Error loading more:', e);
} finally {
loading = false;
}
}
let filterTimeout: any;
function handleFilterChange() {
if (filterTimeout) clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => {
reloadData();
}, 300);
}
function handleRowClick(item: Manifest) {
selectedId = selectedId === item.id ? null : item.id;
}
</script>
<div class="space-y-6 p-6 pb-24">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Manifiestos / Entry's</h1>
<p class="text-muted-foreground">Gestión de manifiestos de exportación</p>
</div>
<Button onclick={() => goto('/dashboard/export/manifest/edit')}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Manifiesto
</Button>
</div>
<!-- Filters Card -->
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2 text-lg">
<Search class="h-5 w-5 text-muted-foreground" /> Filtros
</Card.Title>
<Card.Description>Busca manifiestos por número o descripción</Card.Description>
</Card.Header>
<Card.Content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="filter-num">Número de Manifiesto</Label>
<Input
id="filter-num"
placeholder="Ej: MAN-2024-001"
bind:value={filters.manifest_number}
oninput={handleFilterChange}
/>
</div>
<div class="space-y-2">
<Label for="filter-search">Búsqueda General</Label>
<Input
id="filter-search"
placeholder="Descripción, transportista..."
bind:value={filters.search}
oninput={handleFilterChange}
/>
</div>
</div>
</Card.Content>
</Card.Root>
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- List Card -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title class="text-xl">Listado de Manifiestos</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" size="sm" onclick={reloadData} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
{columns}
{loading}
hasMore={allItems.length < totalItems}
{loadMore}
{selectedId}
onRowClick={handleRowClick}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Sticky Footer Actions -->
<div
class="fixed right-0 bottom-0 left-0 z-[5] 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-6 py-4">
<div class="flex items-center justify-between">
<div class="text-sm text-muted-foreground italic">
{#if selectedItem}
Seleccionado: <span class="font-mono font-bold text-foreground"
>{selectedItem.manifest_number}</span
>
{/if}
</div>
<div class="flex gap-2">
<Button
variant="outline"
size="sm"
onclick={() => (showDetails = true)}
disabled={!selectedItem}
>
<FileText class="mr-2 h-4 w-4" />
Ver Detalles
</Button>
<Button
variant="outline"
size="sm"
onclick={() => selectedItem && goto(`/dashboard/export/manifest/edit/${selectedId}`)}
disabled={!selectedItem}
>
Editar
</Button>
<Button
variant="outline"
size="sm"
onclick={() => (showDelete = true)}
disabled={!selectedItem}
class="text-destructive hover:bg-destructive/5"
>
Borrar
</Button>
</div>
</div>
</div>
</div>
<!-- Dialogs -->
<DeleteDialog bind:open={showDelete} manifest={selectedItem} onSuccess={reloadData} />
<DetailsDialog bind:open={showDetails} manifest={selectedItem} />

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import { page } from '$app/stores';
import ManifestForm from '$lib/components/dashboard/export/manifest/manifest-form.svelte';
// Get ID from params, if any
const id = $derived($page.params.id);
</script>
<ManifestForm {id} />

View File

@@ -0,0 +1,5 @@
<script>
import Maintenance from '$lib/components/common/Maintenance.svelte';
</script>
<Maintenance />

View File

@@ -0,0 +1,5 @@
<script>
import Maintenance from '$lib/components/common/Maintenance.svelte';
</script>
<Maintenance />

View File

@@ -0,0 +1,5 @@
<script>
import Maintenance from '$lib/components/common/Maintenance.svelte';
</script>
<Maintenance />