Se integro el modulo de manifestacion base
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
0
backend/api/v1/modules/a76/manifests/__init__.py
Normal file
0
backend/api/v1/modules/a76/manifests/__init__.py
Normal 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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
41
backend/api/v1/modules/a76/manifests/manifest/debug_db.py
Normal file
41
backend/api/v1/modules/a76/manifests/manifest/debug_db.py
Normal 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()
|
||||
66
backend/api/v1/modules/a76/manifests/manifest/dtos.py
Normal file
66
backend/api/v1/modules/a76/manifests/manifest/dtos.py
Normal 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")
|
||||
52
backend/api/v1/modules/a76/manifests/manifest/models.py
Normal file
52
backend/api/v1/modules/a76/manifests/manifest/models.py
Normal 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)
|
||||
127
backend/api/v1/modules/a76/manifests/manifest/routes.py
Normal file
127
backend/api/v1/modules/a76/manifests/manifest/routes.py
Normal 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
|
||||
192
backend/api/v1/modules/a76/manifests/manifest/service.py
Normal file
192
backend/api/v1/modules/a76/manifests/manifest/service.py
Normal 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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"]
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
33
check_ports.py
Normal 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
33
check_transport_modes.py
Normal 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
83
debug_invoices.py
Normal 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")
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -231,6 +231,8 @@ export interface Invoice {
|
||||
logistics?: InvoiceLogistics;
|
||||
details?: InvoiceSalesDetails[];
|
||||
collections?: InvoiceCollections[];
|
||||
// Client-side only properties
|
||||
is_selected?: boolean;
|
||||
}
|
||||
|
||||
export interface InvoiceListResponse {
|
||||
|
||||
117
frontend/src/lib/api/dashboard/a76/manifests.ts
Normal file
117
frontend/src/lib/api/dashboard/a76/manifests.ts
Normal 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();
|
||||
40
frontend/src/lib/api/dashboard/a76/trailers.ts
Normal file
40
frontend/src/lib/api/dashboard/a76/trailers.ts
Normal 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();
|
||||
40
frontend/src/lib/api/dashboard/a76/transporters.ts
Normal file
40
frontend/src/lib/api/dashboard/a76/transporters.ts
Normal 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();
|
||||
32
frontend/src/lib/api/dashboard/a76/vehicles.ts
Normal file
32
frontend/src/lib/api/dashboard/a76/vehicles.ts
Normal 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}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
@@ -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}/`)
|
||||
};
|
||||
79
frontend/src/lib/components/common/Maintenance.svelte
Normal file
79
frontend/src/lib/components/common/Maintenance.svelte
Normal 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>
|
||||
100
frontend/src/lib/components/dashboard/export/manifest/columns.ts
Normal file
100
frontend/src/lib/components/dashboard/export/manifest/columns.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Maintenance from '$lib/components/common/Maintenance.svelte';
|
||||
</script>
|
||||
|
||||
<Maintenance />
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Maintenance from '$lib/components/common/Maintenance.svelte';
|
||||
</script>
|
||||
|
||||
<Maintenance />
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
};
|
||||
267
frontend/src/routes/dashboard/export/manifest/+page.svelte
Normal file
267
frontend/src/routes/dashboard/export/manifest/+page.svelte
Normal 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} />
|
||||
@@ -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} />
|
||||
5
frontend/src/routes/dashboard/export/repair/+page.svelte
Normal file
5
frontend/src/routes/dashboard/export/repair/+page.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Maintenance from '$lib/components/common/Maintenance.svelte';
|
||||
</script>
|
||||
|
||||
<Maintenance />
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Maintenance from '$lib/components/common/Maintenance.svelte';
|
||||
</script>
|
||||
|
||||
<Maintenance />
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Maintenance from '$lib/components/common/Maintenance.svelte';
|
||||
</script>
|
||||
|
||||
<Maintenance />
|
||||
Reference in New Issue
Block a user