Merge pull request 'feature/pedimentos' (#21) from feature/pedimentos into development

Reviewed-on: ADUANASOFT/anexo76#21
This commit is contained in:
2025-11-24 22:45:03 +00:00
78 changed files with 1993 additions and 2532 deletions

View File

@@ -1,67 +0,0 @@
"""Add timestamp fields to classes table
Revision ID: add_timestamps_classes
Revises: 7937209f9718
Create Date: 2025-11-16 00:20:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'add_timestamps_classes'
down_revision: Union[str, None] = '7937209f9718'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add created_at column with default value
op.execute("""
ALTER TABLE a76.classes
ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
""")
# Add updated_at column with default value
op.execute("""
ALTER TABLE a76.classes
ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
""")
# Add deleted_at column (nullable for soft deletes)
op.execute("""
ALTER TABLE a76.classes
ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE
""")
# Create trigger to auto-update updated_at
op.execute("""
CREATE OR REPLACE FUNCTION a76.update_classes_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
""")
op.execute("""
CREATE TRIGGER update_classes_updated_at
BEFORE UPDATE ON a76.classes
FOR EACH ROW
EXECUTE FUNCTION a76.update_classes_updated_at();
""")
def downgrade() -> None:
# Drop trigger and function
op.execute("DROP TRIGGER IF EXISTS update_classes_updated_at ON a76.classes")
op.execute("DROP FUNCTION IF EXISTS a76.update_classes_updated_at()")
# Drop columns
op.execute("ALTER TABLE a76.classes DROP COLUMN IF EXISTS deleted_at")
op.execute("ALTER TABLE a76.classes DROP COLUMN IF EXISTS updated_at")
op.execute("ALTER TABLE a76.classes DROP COLUMN IF EXISTS created_at")

View File

@@ -4,7 +4,7 @@ Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from decimal import Decimal
from typing import Literal, Optional
from typing import List, Literal, Optional
from pydantic import BaseModel, Field
@@ -119,7 +119,7 @@ class ClientProviderCreateDTO(BaseModel):
is_national_provider: Optional[str] = Field(
None, max_length=2, description="Is national provider"
)
enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status")
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
# Nested DTOs
address: Optional[ClientProviderAddressDTO] = Field(
@@ -162,7 +162,7 @@ class ClientProviderUpdateDTO(BaseModel):
is_national_provider: Optional[str] = Field(
None, max_length=2, description="Is national provider"
)
enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status")
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
# Nested DTOs
address: Optional[ClientProviderAddressDTO] = Field(
@@ -194,7 +194,7 @@ class ClientProviderResponseDTO(BaseModel):
position: Optional[str] = None
incoterm: Optional[str] = None
is_national_provider: Optional[str] = None
enabled_disabled: Optional[int] = None
is_active: Optional[bool] = None
tenant_id: int
company_id: int
@@ -215,7 +215,7 @@ class ClientProviderBasicDTO(BaseModel):
short_name: Optional[str] = None
rfc: Optional[str] = None
client_or_provider: Optional[str] = None
enabled_disabled: Optional[int] = None
is_active: Optional[bool] = None
class Config:
from_attributes = True
@@ -231,3 +231,15 @@ class ClientProviderListDTO(BaseModel):
class Config:
from_attributes = True
class ClientProviderPaginatedResponseDTO(BaseModel):
"""DTO para respuesta paginada de clientes/proveedores"""
items: List[ClientProviderResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -15,7 +15,8 @@ from sqlalchemy import (
PrimaryKeyConstraint,
SmallInteger,
String,
Enum as PgEnum
Enum as PgEnum,
Boolean
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from enum import Enum
@@ -60,8 +61,8 @@ class ClientProvider(Base, TenantScopedMixin):
responsible: Mapped[Optional[str]] = mapped_column(String(80))
position: Mapped[Optional[str]] = mapped_column(String(30))
incoterm: Mapped[Optional[str]] = mapped_column(String(19))
is_national_provider: Mapped[Optional[str]] = mapped_column(String(2))
enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger)
is_national_provider: Mapped[Optional[bool]] = mapped_column(Boolean)
is_active: Mapped[Optional[bool]] = mapped_column(Boolean)
# Relationships
address: Mapped[Optional["ClientProviderAddress"]] = relationship(

View File

@@ -9,136 +9,57 @@ from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import ClientOrProviderEnum
from .dto import (
ClientProviderBasicDTO,
ClientProviderCreateDTO,
ClientProviderResponseDTO,
ClientProviderUpdateDTO,
ClientProviderPaginatedResponseDTO,
)
from .service import ClientProviderService
from .models import ClientProvider
# Create base CRUD router using TenantCRUDRoutes factory
base_router = TenantCRUDRoutes(
service=ClientProviderService,
create_schema=ClientProviderCreateDTO,
update_schema=ClientProviderUpdateDTO,
response_schema=ClientProviderResponseDTO,
prefix="",
tags=[],
resource_name="Client/Provider",
id_name="id", # Using numeric ID
enable_list=True, # Enable GET /clients-providers with pagination
enable_filters=True, # Enable filtering
default_page_size=50,
max_page_size=100,
).router
# Create main router to add custom endpoints
router = APIRouter(prefix="/clients-providers")
# Include base CRUD routes
router.include_router(base_router, prefix="")
# Custom endpoints
@router.get("/clients", response_model=List[ClientProviderBasicDTO])
async def get_clients_only(
@router.get("/", response_model=ClientProviderPaginatedResponseDTO)
async def get_clients_and_providers(
company_id: int = Query(..., description="Company ID"),
type: Optional[ClientOrProviderEnum] = Query(
None, description="Type of entity (client or provider)"
),
active: Optional[bool] = Query(None, description="Active status"),
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),
):
"""Get only clients (client_or_provider = 'client')"""
"""Get clients and providers"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
clients = (
db.query(ClientProvider)
.filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
ClientProvider.client_or_provider == "client"
)
.offset(skip)
.limit(limit)
.all()
query = db.query(ClientProvider).filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
return [ClientProviderBasicDTO.model_validate(c) for c in clients]
if type is not None:
query = query.filter(ClientProvider.client_or_provider == type)
@router.get("/providers", response_model=List[ClientProviderBasicDTO])
async def get_providers_only(
company_id: int = Query(..., description="Company 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),
):
"""Get only providers (client_or_provider = 'provider')"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
providers = (
db.query(ClientProvider)
.filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
ClientProvider.client_or_provider == "provider"
)
.offset(skip)
.limit(limit)
.all()
)
return [ClientProviderBasicDTO.model_validate(p) for p in providers]
if active is not None:
query = query.filter(ClientProvider.is_active == active)
total = query.count()
clients = query.offset(skip).limit(limit).all()
@router.get("/search/rfc/{rfc}", response_model=List[ClientProviderBasicDTO])
async def search_by_rfc(
rfc: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Search clients/providers by RFC"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
clients = (
db.query(ClientProvider)
.filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
ClientProvider.rfc.ilike(f"%{rfc}%")
)
.all()
)
return [ClientProviderBasicDTO.model_validate(c) for c in clients]
@router.patch("/{client_id}/toggle-status", response_model=ClientProviderResponseDTO)
async def toggle_clients_and_providers_status(
client_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Toggle client/provider enabled/disabled status"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
if not client:
raise HTTPException(status_code=404, detail="Client/Provider not found")
# Toggle status (1 = enabled, 0 = disabled)
client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0
try:
db.commit()
db.refresh(client)
return client
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail="Error updating status")
return {
"items": [ClientProviderResponseDTO.model_validate(c) for c in clients],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO)
@@ -150,9 +71,59 @@ async def get_clients_and_providers_basic_info(
):
"""Get basic information for a client/provider (without address and programs)"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
if not client:
raise HTTPException(status_code=404, detail="Client/Provider not found")
return ClientProviderBasicDTO.model_validate(client)
@router.post("/", response_model=ClientProviderResponseDTO)
async def create_client_provider(
client_data: ClientProviderCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Create a new client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ClientProviderService.create(db, client_data, tenant_id, company_id)
@router.patch("/{client_id}", response_model=ClientProviderResponseDTO)
async def update_client_provider(
client_id: int,
client_data: ClientProviderUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Update a client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
client = ClientProviderService.update(
db, client_id, tenant_id, company_id, client_data
)
if not client:
raise HTTPException(status_code=404, detail="Client/Provider not found")
return client
@router.delete("/{client_id}", response_model=bool)
async def delete_client_provider(
client_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Delete a client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ClientProviderService.delete(db, client_id, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Client/Provider not found")
return success

View File

@@ -61,13 +61,17 @@ class ClientProviderService:
)
if filters.get("status"):
enabled = 1 if filters["status"] == "enabled" else 0
query = query.filter(ClientProvider.enabled_disabled == enabled)
query = query.filter(ClientProvider.is_active == enabled)
total = query.count()
clients = query.options(
joinedload(ClientProvider.address),
joinedload(ClientProvider.programs)
).offset(skip).limit(limit).all()
clients = (
query.options(
joinedload(ClientProvider.address), joinedload(ClientProvider.programs)
)
.offset(skip)
.limit(limit)
.all()
)
return clients, total
@@ -79,8 +83,7 @@ class ClientProviderService:
return (
db.query(ClientProvider)
.options(
joinedload(ClientProvider.address),
joinedload(ClientProvider.programs)
joinedload(ClientProvider.address), joinedload(ClientProvider.programs)
)
.filter(
ClientProvider.id == client_id,
@@ -102,9 +105,7 @@ class ClientProviderService:
# Create main client/provider
data_dict = client_data.model_dump(exclude={"address", "programs"})
db_client = ClientProvider(
**data_dict,
tenant_id=tenant_id,
company_id=company_id
**data_dict, tenant_id=tenant_id, company_id=company_id
)
db.add(db_client)
@@ -112,8 +113,8 @@ class ClientProviderService:
# Create address if provided
if client_data.address:
db_address = ClientProviderAddress(
tenant_id=tenant_id,
db_address = ClientProviderAddress(
tenant_id=tenant_id,
company_id=company_id,
client_id=db_client.id,
**client_data.address.model_dump(exclude_unset=True),
@@ -122,7 +123,7 @@ class ClientProviderService:
# Create programs if provided
if client_data.programs:
db_programs = ClientProviderPrograms(
db_programs = ClientProviderPrograms(
tenant_id=tenant_id,
company_id=company_id,
client_id=db_client.id,
@@ -210,9 +211,7 @@ class ClientProviderService:
)
@staticmethod
def delete(
db: Session, client_id: int, tenant_id: int, company_id: int
) -> bool:
def delete(db: Session, client_id: int, tenant_id: int, company_id: int) -> bool:
"""Delete a client/provider"""
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
if not client:
@@ -351,7 +350,7 @@ class ClientProviderService:
)
if enabled_only:
query = query.filter(ClientProvider.enabled_disabled == 1)
query = query.filter(ClientProvider.is_active == 1)
# Contar total
total = query.count()
@@ -478,59 +477,4 @@ class ClientProviderService:
logger.error(f"Error deleting client/provider {client_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error deleting client/provider"
)
def get_clients_only(
self, skip: int = 0, limit: int = 100
) -> List[ClientProviderBasicDTO]:
"""Obtiene solo clientes (client)"""
query = self.db.query(ClientProvider).filter(
ClientProvider.client_or_provider == "client"
)
clients = query.offset(skip).limit(limit).all()
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
def get_providers_only(
self, skip: int = 0, limit: int = 100
) -> List[ClientProviderBasicDTO]:
"""Obtiene solo proveedores (provider)"""
query = self.db.query(ClientProvider).filter(
ClientProvider.client_or_provider == "provider"
)
providers = query.offset(skip).limit(limit).all()
return [
ClientProviderBasicDTO.model_validate(provider) for provider in providers
]
def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]:
"""Busca clientes/proveedores por RFC"""
clients = (
self.db.query(ClientProvider)
.filter(ClientProvider.rfc.ilike(f"%{rfc}%"))
.all()
)
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]:
"""Cambia el estado habilitado/deshabilitado"""
client = (
self.db.query(ClientProvider)
.filter(ClientProvider.client_id == client_id)
.first()
)
if not client:
return None
# Toggle status (1 = habilitado, 0 = deshabilitado)
client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0
try:
self.db.commit()
logger.info(
f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}"
)
return self._get_client_with_relations(client_id)
except Exception as e:
self.db.rollback()
logger.error(f"Error toggling status for {client_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating status")
)

View File

@@ -67,7 +67,7 @@ class PartCreateDTO(BaseModel):
added_value: Optional[Decimal] = Field(None, description="Added value")
# Status and media
enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status")
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
creation_date: Optional[int] = Field(None, description="Creation date")
part_photo: Optional[str] = Field(
None, max_length=255, description="Part photo URL"
@@ -132,7 +132,7 @@ class PartUpdateDTO(BaseModel):
added_value: Optional[Decimal] = Field(None, description="Added value")
# Status and media
enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status")
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
part_photo: Optional[str] = Field(
None, max_length=255, description="Part photo URL"
)
@@ -178,7 +178,7 @@ class PartResponseDTO(BaseModel):
added_value: Optional[Decimal] = None
# Status and dates
enabled_disabled: Optional[int] = None
is_active: Optional[bool] = None
creation_date: Optional[int] = None
modification_date: Optional[int] = None
modification_date_iso: Optional[datetime] = None
@@ -200,7 +200,7 @@ class PartBasicDTO(BaseModel):
part_class: Optional[str] = None
unit_cost: Optional[Decimal] = None
currency_key: Optional[str] = None
enabled_disabled: Optional[int] = None
is_active: Optional[bool] = None
class Config:
from_attributes = True

View File

@@ -16,6 +16,7 @@ from sqlalchemy import (
SmallInteger,
String,
UniqueConstraint,
Boolean,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -90,7 +91,7 @@ class Part(Base, TenantScopedMixin):
added_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
# Status and dates
enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger)
is_active: Mapped[Optional[bool]] = mapped_column(Boolean)
creation_date: Mapped[Optional[int]] = mapped_column() # FECHACREACIONPARTE
modification_date: Mapped[Optional[int]] = mapped_column() # FECHAMODIFICA
modification_date_iso: Mapped[Optional[datetime]] = (

View File

@@ -348,7 +348,7 @@ async def get_part_basic_info(
part_class=part.part_class,
unit_cost=part.unit_cost,
currency_key=part.currency_key,
enabled_disabled=part.enabled_disabled,
is_active=part.is_active,
)

View File

@@ -221,7 +221,7 @@ class PartService:
return None
# Toggle status (assuming 1 = enabled, 0 = disabled)
db_part.enabled_disabled = 1 if db_part.enabled_disabled == 0 else 0
db_part.is_active = 1 if db_part.is_active == 0 else 0
db.commit()
db.refresh(db_part)
@@ -257,8 +257,8 @@ class PartService:
)
# Partes habilitadas vs deshabilitadas
enabled_parts = db.query(Part).filter(Part.enabled_disabled == 1).count()
disabled_parts = db.query(Part).filter(Part.enabled_disabled == 0).count()
enabled_parts = db.query(Part).filter(Part.is_active == 1).count()
disabled_parts = db.query(Part).filter(Part.is_active == 0).count()
return {
"total_parts": total_parts,

View File

@@ -7,9 +7,8 @@ from pydantic import BaseModel, ConfigDict, Field
class PedimentoDatesBase(BaseModel):
"""Base schema for Pedimento Dates"""
entry_date: Optional[datetime] = Field(None, description="Entry date")
pedimento_date: Optional[datetime] = Field(None, description="Pedimento date")
payment_date: Optional[datetime] = Field(None, description="Payment date")
entry_date: Optional[datetime] = Field(None, description="Entry date")
payment_date: datetime = Field(None, description="Payment date")
rectification_payment_date: Optional[datetime] = Field(
None, description="Rectification payment date"
)
@@ -19,21 +18,26 @@ class PedimentoDatesBase(BaseModel):
original_date: Optional[datetime] = Field(None, description="Original date")
start_date: Optional[datetime] = Field(None, description="Start date")
end_date: Optional[datetime] = Field(None, description="End date")
capture_date: Optional[datetime] = Field(None, description="Capture date")
capture_time: Optional[time] = Field(None, description="Capture time")
class PedimentoDatesCreate(PedimentoDatesBase):
"""Schema for creating a new Pedimento Dates"""
class PedimentoDatesCreate(BaseModel):
"""Schema for creating a new Pedimento Dates - pedimento_id and tenant_id are set by backend"""
pass
entry_date: Optional[datetime] = Field(None, description="Entry date")
payment_date: datetime = Field(..., description="Payment date")
rectification_payment_date: Optional[datetime] = Field(None, description="Rectification payment date")
extraction_date: Optional[datetime] = Field(None, description="Extraction date")
submission_date: Optional[datetime] = Field(None, description="Submission date")
eucan_date: Optional[datetime] = Field(None, description="EUCAN date")
original_date: Optional[datetime] = Field(None, description="Original date")
start_date: Optional[datetime] = Field(None, description="Start date")
end_date: Optional[datetime] = Field(None, description="End date")
class PedimentoDatesUpdate(BaseModel):
"""Schema for updating a Pedimento Dates"""
entry_date: Optional[datetime] = None
pedimento_date: Optional[datetime] = None
entry_date: Optional[datetime] = None
payment_date: Optional[datetime] = None
rectification_payment_date: Optional[datetime] = None
extraction_date: Optional[datetime] = None
@@ -42,8 +46,6 @@ class PedimentoDatesUpdate(BaseModel):
original_date: Optional[datetime] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
capture_date: Optional[datetime] = None
capture_time: Optional[time] = None
class PedimentoDatesResponse(PedimentoDatesBase):

View File

@@ -25,14 +25,23 @@ class PedimentoPaymentsBase(BaseModel):
total_cash_paid: Optional[int] = Field(None, description="Total cash paid")
total_contributions: Optional[int] = Field(None, description="Total contributions")
counter_payment: Optional[int] = Field(None, description="Counter payment")
pece_code: Optional[str] = Field(None, max_length=5, description="PECE code")
payment_id: Optional[int] = Field(None, description="Payment ID")
pece_code: Optional[str] = Field(None, max_length=5, description="PECE code")
class PedimentoPaymentsCreate(PedimentoPaymentsBase):
"""Schema for creating a new Pedimento Payments"""
class PedimentoPaymentsCreate(BaseModel):
"""Schema for creating a new Pedimento Payments - pedimento_id and tenant_id are set by backend"""
pass
acknowledgment: Optional[str] = Field(None, max_length=20, description="Acknowledgment")
operation_number: Optional[str] = Field(None, max_length=14, description="Operation number")
bank_code: Optional[int] = Field(None, description="Bank code")
cashier: Optional[str] = Field(None, max_length=2, description="Cashier")
date: Optional[Date] = Field(None, description="Date")
time: Optional[Time] = Field(None, description="Time")
shift: Optional[str] = Field(None, max_length=1, description="Shift")
total_cash_paid: Optional[int] = Field(None, description="Total cash paid")
total_contributions: Optional[int] = Field(None, description="Total contributions")
counter_payment: Optional[int] = Field(None, description="Counter payment")
pece_code: Optional[str] = Field(None, max_length=5, description="PECE code")
class PedimentoPaymentsUpdate(BaseModel):
@@ -48,8 +57,7 @@ class PedimentoPaymentsUpdate(BaseModel):
total_cash_paid: Optional[int] = None
total_contributions: Optional[int] = None
counter_payment: Optional[int] = None
pece_code: Optional[str] = Field(None, max_length=5)
payment_id: Optional[int] = None
pece_code: Optional[str] = Field(None, max_length=5)
class PedimentoPaymentsResponse(PedimentoPaymentsBase):

View File

@@ -15,10 +15,13 @@ class PedimentoTransportMeansBase(BaseModel):
departure: Optional[str] = Field(None, max_length=2, description="Departure")
class PedimentoTransportMeansCreate(PedimentoTransportMeansBase):
"""Schema for creating a new Pedimento Transport Means"""
class PedimentoTransportMeansCreate(BaseModel):
"""Schema for creating a new Pedimento Transport Means - pedimento_id and tenant_id are set by backend"""
pass
destination: Optional[int] = Field(None, description="Destination")
entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit")
arrival: Optional[str] = Field(None, max_length=2, description="Arrival")
departure: Optional[str] = Field(None, max_length=2, description="Departure")
class PedimentoTransportMeansUpdate(BaseModel):

View File

@@ -27,10 +27,17 @@ class PedimentoValidationBase(BaseModel):
responsible_id: Optional[int] = Field(None, description="Responsible ID")
class PedimentoValidationCreate(PedimentoValidationBase):
"""Schema for creating a new Pedimento Validation"""
class PedimentoValidationCreate(BaseModel):
"""Schema for creating a new Pedimento Validation - pedimento_id and tenant_id are set by backend"""
pass
validator: Optional[str] = Field(None, max_length=3, description="Validator")
validation_ack: Optional[str] = Field(None, max_length=8, description="Validation acknowledgment")
pre_ack: Optional[str] = Field(None, max_length=8, description="Previous acknowledgment")
line_signature: Optional[str] = Field(None, max_length=50, description="Line signature")
electronic_signature: Optional[str] = Field(None, max_length=999, description="Electronic signature")
certificate_number: Optional[str] = Field(None, max_length=99, description="Certificate number")
validator_id: Optional[int] = Field(None, description="Validator ID")
responsible_id: Optional[int] = Field(None, description="Responsible ID")
class PedimentoValidationUpdate(BaseModel):

View File

@@ -5,22 +5,22 @@ from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from .pedimento_config_additional import PedimentoConfigAdditionalCreate
from .pedimento_config_calculations import PedimentoConfigCalculationsCreate
from .pedimento_config_parameters import PedimentoConfigParametersCreate
from .pedimento_config_surcharges import PedimentoConfigSurchargesCreate
from .pedimento_config_update_rectification import PedimentoConfigUpdateRectificationCreate
from .pedimento_config_updates import PedimentoConfigUpdatesCreate
from .pedimento_customs_offices import PedimentoCustomsOfficesCreate
from .pedimento_dates import PedimentoDatesCreate
from .pedimento_decrementables import PedimentoDecrementablesCreate
from .pedimento_incrementables import PedimentoIncrementablesCreate
from .pedimento_indexes import PedimentoIndexesCreate
from .pedimento_payments import PedimentoPaymentsCreate
from .pedimento_rectification_destination import PedimentoRectificationDestinationCreate
from .pedimento_rectification_origin import PedimentoRectificationOriginCreate
from .pedimento_transport_means import PedimentoTransportMeansCreate
from .pedimento_validation import PedimentoValidationCreate
from .pedimento_config_additional import PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalResponse
from .pedimento_config_calculations import PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsResponse
from .pedimento_config_parameters import PedimentoConfigParametersCreate, PedimentoConfigParametersResponse
from .pedimento_config_surcharges import PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesResponse
from .pedimento_config_update_rectification import PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationResponse
from .pedimento_config_updates import PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesResponse
from .pedimento_customs_offices import PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesResponse
from .pedimento_dates import PedimentoDatesCreate, PedimentoDatesResponse
from .pedimento_decrementables import PedimentoDecrementablesCreate, PedimentoDecrementablesResponse
from .pedimento_incrementables import PedimentoIncrementablesCreate, PedimentoIncrementablesResponse
from .pedimento_indexes import PedimentoIndexesCreate, PedimentoIndexesResponse
from .pedimento_payments import PedimentoPaymentsCreate, PedimentoPaymentsResponse
from .pedimento_rectification_destination import PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationResponse
from .pedimento_rectification_origin import PedimentoRectificationOriginCreate, PedimentoRectificationOriginResponse
from .pedimento_transport_means import PedimentoTransportMeansCreate, PedimentoTransportMeansResponse
from .pedimento_validation import PedimentoValidationCreate, PedimentoValidationResponse
class OperationType(IntEnum):
@@ -50,8 +50,22 @@ class PedimentosBase(BaseModel):
usd_value: Optional[Decimal] = Field(None, description="USD value")
paid_price: Optional[Decimal] = Field(None, description="Paid price")
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
class PedimentosCreate(PedimentosBase):
"""Schema for creating a new Pedimento"""
# Override to make required fields non-optional
year: str = Field(..., max_length=2, description="Year")
customs_office: str = Field(..., max_length=2, description="Customs office")
license: str = Field(..., max_length=4, description="License")
pedimento_number: str = Field(..., max_length=7, description="Pedimento number")
client_id: int = Field(..., description="Client ID")
operation_type: int = Field(..., description="Operation type")
pedimento_type: int = Field(..., description="Pedimento type")
regime: str = Field(..., max_length=3, description="Regime")
status: str = Field(..., max_length=30, description="Status")
pedimento_dates: Optional[PedimentoDatesCreate] = None
pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None
pedimento_incrementables: Optional[PedimentoIncrementablesCreate] = None
@@ -69,37 +83,41 @@ class PedimentosBase(BaseModel):
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationCreate] = None
pedimento_config_updates: Optional[PedimentoConfigUpdatesCreate] = None
class PedimentosCreate(PedimentosBase):
"""Schema for creating a new Pedimento"""
# Override to make required fields non-optional
year: str = Field(..., max_length=2, description="Year")
customs_office: str = Field(..., max_length=2, description="Customs office")
license: str = Field(..., max_length=4, description="License")
pedimento_number: str = Field(..., max_length=7, description="Pedimento number")
client_id: int = Field(..., description="Client ID")
operation_type: int = Field(..., description="Operation type")
pedimento_type: int = Field(..., description="Pedimento type")
regime: str = Field(..., max_length=3, description="Regime")
status: str = Field(..., max_length=30, description="Status")
class PedimentosUpdate(BaseModel):
"""Schema for updating a Pedimento"""
year: Optional[str] = Field(..., max_length=2)
customs_office: Optional[str] = Field(..., max_length=2)
license: Optional[str] = Field(..., max_length=4)
pedimento_number: Optional[str] = Field(..., max_length=7)
client_id: Optional[int]
operation_type: Optional[OperationType]
pedimento_type: Optional[int]
pedimento_code: Optional[str] = Field(..., max_length=2)
regime: Optional[str] = Field(..., max_length=3)
status: Optional[str] = Field(..., max_length=30)
year: Optional[str] = Field(None, max_length=2)
customs_office: Optional[str] = Field(None, max_length=2)
license: Optional[str] = Field(None, max_length=4)
pedimento_number: Optional[str] = Field(None, max_length=7)
client_id: Optional[int] = None
operation_type: Optional[int] = None
pedimento_type: Optional[int] = None
pedimento_code: Optional[str] = Field(None, max_length=2)
regime: Optional[str] = Field(None, max_length=3)
status: Optional[str] = Field(None, max_length=30)
usd_value: Optional[Decimal] = None
paid_price: Optional[Decimal] = None
gross_weight: Optional[Decimal] = None
exchange_rate: Optional[Decimal] = None
# Sub-resources
pedimento_dates: Optional[PedimentoDatesCreate] = None
pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None
pedimento_incrementables: Optional[PedimentoIncrementablesCreate] = None
pedimento_indexes: Optional[PedimentoIndexesCreate] = None
pedimento_validation: Optional[PedimentoValidationCreate] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
pedimento_payments: Optional[PedimentoPaymentsCreate] = None
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationCreate] = None
pedimento_rectification_origin: Optional[PedimentoRectificationOriginCreate] = None
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
pedimento_config_additional: Optional[PedimentoConfigAdditionalCreate] = None
pedimento_config_calculations: Optional[PedimentoConfigCalculationsCreate] = None
pedimento_config_parameters: Optional[PedimentoConfigParametersCreate] = None
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesCreate] = None
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationCreate] = None
pedimento_config_updates: Optional[PedimentoConfigUpdatesCreate] = None
class PedimentosResponse(PedimentosBase):
@@ -108,5 +126,22 @@ class PedimentosResponse(PedimentosBase):
id: int
tenant_id: int
created_at: datetime
pedimento_dates: Optional[PedimentoDatesResponse] = None
pedimento_decrementables: Optional[PedimentoDecrementablesResponse] = None
pedimento_incrementables: Optional[PedimentoIncrementablesResponse] = None
pedimento_indexes: Optional[PedimentoIndexesResponse] = None
pedimento_validation: Optional[PedimentoValidationResponse] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesResponse] = None
pedimento_payments: Optional[PedimentoPaymentsResponse] = None
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationResponse] = None
pedimento_rectification_origin: Optional[PedimentoRectificationOriginResponse] = None
pedimento_transport_means: Optional[PedimentoTransportMeansResponse] = None
pedimento_config_additional: Optional[PedimentoConfigAdditionalResponse] = None
pedimento_config_calculations: Optional[PedimentoConfigCalculationsResponse] = None
pedimento_config_parameters: Optional[PedimentoConfigParametersResponse] = None
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesResponse] = None
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationResponse] = None
pedimento_config_updates: Optional[PedimentoConfigUpdatesResponse] = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from datetime import time as datetime_time
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
@@ -48,16 +48,16 @@ class PedimentoDates(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(Integer)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
entry_date: Mapped[datetime] = mapped_column(DateTime)
entry_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
pedimento_date: Mapped[datetime] = mapped_column(DateTime)
payment_date: Mapped[datetime] = mapped_column(DateTime)
rectification_payment_date: Mapped[datetime] = mapped_column(DateTime)
extraction_date: Mapped[datetime] = mapped_column(DateTime)
submission_date: Mapped[datetime] = mapped_column(DateTime)
eucan_date: Mapped[datetime] = mapped_column(DateTime)
original_date: Mapped[datetime] = mapped_column(DateTime)
start_date: Mapped[datetime] = mapped_column(DateTime)
end_date: Mapped[datetime] = mapped_column(DateTime)
rectification_payment_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
extraction_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
submission_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
eucan_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
original_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
start_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
end_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
capture_date: Mapped[datetime] = mapped_column(DateTime)
capture_time: Mapped[datetime_time] = mapped_column(Time)

View File

@@ -49,7 +49,6 @@ class PedimentoPayments(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(Integer)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
payment_id: Mapped[int] = mapped_column(Integer)
acknowledgment: Mapped[str] = mapped_column(String(20))
operation_number: Mapped[str] = mapped_column(String(14))

View File

@@ -35,10 +35,10 @@ async def list_payments(
return payments
@router.get("/{payment_id}", response_model=PedimentoPaymentsResponse)
@router.get("/{id}", response_model=PedimentoPaymentsResponse)
async def get_payment(
pedimento_id: int,
payment_id: int,
id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
@@ -47,7 +47,7 @@ async def get_payment(
tenant_id = validate_access_to_resource(db, company_id, current_user)
payment = PedimentoPaymentsService.get_by_id(
db, payment_id, pedimento_id, tenant_id, company_id
db, id, pedimento_id, tenant_id, company_id
)
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
@@ -74,10 +74,10 @@ async def create_payment(
return payment
@router.put("/{payment_id}", response_model=PedimentoPaymentsResponse)
@router.put("/{id}", response_model=PedimentoPaymentsResponse)
async def update_payment(
pedimento_id: int,
payment_id: int,
id: int,
data: PedimentoPaymentsUpdate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
@@ -87,7 +87,7 @@ async def update_payment(
tenant_id = validate_access_to_resource(db, company_id, current_user)
payment = PedimentoPaymentsService.update(
db, payment_id, pedimento_id, tenant_id, company_id, data
db, id, pedimento_id, tenant_id, company_id, data
)
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
@@ -95,10 +95,10 @@ async def update_payment(
return payment
@router.delete("/{payment_id}", status_code=204)
@router.delete("/{id}", status_code=204)
async def delete_payment(
pedimento_id: int,
payment_id: int,
id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
@@ -107,7 +107,7 @@ async def delete_payment(
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = PedimentoPaymentsService.delete(
db, payment_id, pedimento_id, tenant_id, company_id
db, id, pedimento_id, tenant_id, company_id
)
if not success:
raise HTTPException(status_code=404, detail="Payment not found")

View File

@@ -6,7 +6,8 @@ import logging
from typing import Any, Dict, List, Optional
from sqlalchemy import desc
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.orm import selectinload
from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate
@@ -85,8 +86,31 @@ class PedimentosService:
query = query.filter(Pedimentos.year == filters["year"])
total = query.count()
# Eager load all relationships for the response schema
items = (
query.order_by(desc(Pedimentos.created_at)).offset(skip).limit(limit).all()
query.options(
selectinload(Pedimentos.pedimento_dates),
selectinload(Pedimentos.pedimento_decrementables),
selectinload(Pedimentos.pedimento_incrementables),
selectinload(Pedimentos.pedimento_indexes),
selectinload(Pedimentos.pedimento_validation),
selectinload(Pedimentos.pedimento_customs_offices),
selectinload(Pedimentos.pedimento_payments),
selectinload(Pedimentos.pedimento_rectification_destination),
selectinload(Pedimentos.pedimento_rectification_origin),
selectinload(Pedimentos.pedimento_transport_means),
selectinload(Pedimentos.pedimento_config_additional),
selectinload(Pedimentos.pedimento_config_calculations),
selectinload(Pedimentos.pedimento_config_parameters),
selectinload(Pedimentos.pedimento_config_surcharges),
selectinload(Pedimentos.pedimento_config_update_rectification),
selectinload(Pedimentos.pedimento_config_updates),
)
.order_by(desc(Pedimentos.created_at))
.offset(skip)
.limit(limit)
.all()
)
return items, total
@@ -113,6 +137,26 @@ class PedimentosService:
if company_id is not None:
query = query.filter(Pedimentos.company_id == company_id)
# Eager load all relationships for the response schema
query = query.options(
selectinload(Pedimentos.pedimento_dates),
selectinload(Pedimentos.pedimento_decrementables),
selectinload(Pedimentos.pedimento_incrementables),
selectinload(Pedimentos.pedimento_indexes),
selectinload(Pedimentos.pedimento_validation),
selectinload(Pedimentos.pedimento_customs_offices),
selectinload(Pedimentos.pedimento_payments),
selectinload(Pedimentos.pedimento_rectification_destination),
selectinload(Pedimentos.pedimento_rectification_origin),
selectinload(Pedimentos.pedimento_transport_means),
selectinload(Pedimentos.pedimento_config_additional),
selectinload(Pedimentos.pedimento_config_calculations),
selectinload(Pedimentos.pedimento_config_parameters),
selectinload(Pedimentos.pedimento_config_surcharges),
selectinload(Pedimentos.pedimento_config_update_rectification),
selectinload(Pedimentos.pedimento_config_updates),
)
return query.first()
@@ -247,23 +291,25 @@ class PedimentosService:
# Helper function para actualizar o crear objetos relacionados
def update_or_create_related(service_class, model_class, data_attr):
if not hasattr(pedimento_data, data_attr):
# Obtener datos del payload completo (no solo exclude_unset)
full_data = pedimento_data.model_dump()
if data_attr not in full_data:
return
data = getattr(pedimento_data, data_attr)
data = full_data[data_attr]
if not data:
return
existing = service_class.get_by_pedimento_id(db, pedimento_id, tenant_id)
if existing:
# Actualizar existente
update_dict = data.model_dump(exclude_unset=True)
for field, value in update_dict.items():
setattr(existing, field, value)
for field, value in data.items():
if hasattr(existing, field):
setattr(existing, field, value)
else:
# Crear nuevo
obj_dict = data.model_dump()
obj = model_class(**obj_dict)
obj = model_class(**data)
obj.pedimento_id = pedimento_id
obj.tenant_id = tenant_id
obj.company_id = company_id

View File

@@ -15,11 +15,22 @@ router = APIRouter(prefix="/code-pedimento-regimens")
def list_code_pedimento_regimens(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
code: str = Query(None, description="Filter by code"),
regime: str = Query(None, description="Filter by regime"),
type: str = Query(None, description="Filter by type"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
skip = (page - 1) * page_size
query = db.query(CodePedimentoRegimen)
if code is not None:
query = query.filter(CodePedimentoRegimen.pedimento_code == code)
if regime is not None:
query = query.filter(CodePedimentoRegimen.regime == regime)
if type is not None:
query = query.filter(CodePedimentoRegimen.type == type)
items = query.offset(skip).limit(page_size).all()
total = query.count()
return {

View File

@@ -31,8 +31,8 @@ export interface ClientProvider {
domicile_fiscal?: string | null;
foreign_tax_id?: string | null;
client_or_provider?: string | null;
enabled_disabled?: number;
tenant_id: number;
is_active?: boolean;
tenant_id: number;
address?: ClientProviderAddress | null;
programs?: ClientProviderPrograms | null;
}
@@ -46,7 +46,7 @@ export interface ClientProviderBasic {
domicile_fiscal?: string | null;
foreign_tax_id?: string | null;
client_or_provider?: string | null;
enabled_disabled?: number;
is_active?: boolean;
tenant_id: number;
}
@@ -65,7 +65,7 @@ export interface CreateClientProviderData {
domicile_fiscal?: string | null;
foreign_tax_id?: string | null;
client_or_provider?: string | null;
enabled_disabled?: number;
is_active?: boolean;
address?: Omit<ClientProviderAddress, 'id' | 'client_id'> | null;
programs?: Omit<ClientProviderPrograms, 'id' | 'client_id'> | null;
}
@@ -78,7 +78,7 @@ export interface UpdateClientProviderData {
domicile_fiscal?: string | null;
foreign_tax_id?: string | null;
client_or_provider?: string | null;
enabled_disabled?: number;
is_active?: boolean;
address?: Partial<ClientProviderAddress> | null;
programs?: Partial<ClientProviderPrograms> | null;
}
@@ -100,7 +100,7 @@ export const clientsProvidersApi = {
page: page.toString(),
page_size: pageSize.toString()
});
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
@@ -108,7 +108,7 @@ export const clientsProvidersApi = {
}
});
}
return api.get<ClientProviderListResponse>(
`/v1/a76/clients-providers?${params.toString()}`
);
@@ -166,7 +166,7 @@ export const clientsProvidersApi = {
/**
* Crea un nuevo cliente/proveedor
* @param companyId - ID de la compañía
* @param companyId - ID de la compañía
* @param data - Datos del cliente/proveedor a crear
*/
create: (companyId: number, data: CreateClientProviderData) =>

View File

@@ -19,7 +19,7 @@ export interface CustomsBroker {
tax_id?: string | null;
personal_id?: string | null;
position?: string | null;
license?: string | null;
license: string;
company?: string | null;
contact?: string | null;
tenant_id: string;
@@ -117,6 +117,14 @@ export const customsBrokersApi = {
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
},
/**
* 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 VU de un agente aduanal
*/

View File

@@ -17,8 +17,6 @@ export interface PedimentoDates {
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
capture_date?: string | null;
capture_time?: string | null;
created_at: string;
}
@@ -33,8 +31,6 @@ export interface CreatePedimentoDatesData {
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
capture_date?: string | null;
capture_time?: string | null;
}
export interface UpdatePedimentoDatesData {
@@ -48,8 +44,6 @@ export interface UpdatePedimentoDatesData {
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
capture_date?: string | null;
capture_time?: string | null;
}
export const pedimentoDatesApi = {

View File

@@ -17,8 +17,7 @@ export interface PedimentoPayments {
total_cash_paid?: number | null;
total_contributions?: number | null;
counter_payment?: number | null;
pece_code?: string | null;
payment_id?: number | null;
pece_code?: string | null;
created_at: string;
}
@@ -33,8 +32,7 @@ export interface CreatePedimentoPaymentsData {
total_cash_paid?: number | null;
total_contributions?: number | null;
counter_payment?: number | null;
pece_code?: string | null;
payment_id?: number | null;
pece_code?: string | null;
}
export interface UpdatePedimentoPaymentsData {
@@ -48,8 +46,7 @@ export interface UpdatePedimentoPaymentsData {
total_cash_paid?: number | null;
total_contributions?: number | null;
counter_payment?: number | null;
pece_code?: string | null;
payment_id?: number | null;
pece_code?: string | null;
}
export const pedimentoPaymentsApi = {

View File

@@ -9,22 +9,45 @@ export interface PedimentoDates {
entry_date?: string | null;
pedimento_date?: string | null;
payment_date?: string | null;
rectification_payment_date?: string | null;
extraction_date?: string | null;
submission_date?: string | null;
eucan_date?: string | null;
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
}
export interface PedimentoPayments {
payment_form?: string | null;
bank_identifier?: string | null;
acknowledgment?: string | null;
operation_number?: string | null;
bank_code?: string | null;
cashier?: string | null;
date?: string | null;
time?: string | null;
shift?: string | null;
total_cash_paid?: string | null;
total_contributions?: string | null;
counter_payment?: string | null;
pece_code?: string | null;
}
export interface PedimentoTransportMeans {
arrival_key?: string | null;
arrival_data?: string | null;
departure_key?: string | null;
departure_data?: string | null;
destination?: number | null;
entry_exit?: string | null;
arrival?: string | null;
departure?: string | null;
}
export interface PedimentoValidation {
document?: string | null;
validator?: string | null;
validation_ack?: string | null;
pre_ack?: string | null;
line_signature?: string | null;
electronic_signature?: string | null;
certificate_number?: string | null;
validator_id?: number | null;
responsible_id?: number | null;
}
export interface Pedimento {
@@ -49,7 +72,7 @@ export interface Pedimento {
pedimento_dates?: PedimentoDates | null;
pedimento_payments?: PedimentoPayments | null;
pedimento_transport_means?: PedimentoTransportMeans | null;
pedimento_validation?: PedimentoValidation | null;
pedimento_validation?: PedimentoValidation | null;
}
export interface PedimentoListResponse {

View File

@@ -9,6 +9,7 @@
import { materialTypesApi, type MaterialType } from "$lib/api/dashboard/refrence_data/material_types";
import { companyStore } from "$lib/stores/company.svelte";
import { onMount } from 'svelte';
import { LoaderCircle, Home } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -248,21 +249,7 @@
{#if companyStore.activeCompany}
<div class="rounded-md bg-blue-50 border border-blue-200 p-3">
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-blue-600"
>
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
<Home class="h-4 w-4 text-blue-600" />
<div>
<p class="text-sm font-medium text-blue-900">
{companyStore.activeCompany.name}
@@ -426,26 +413,7 @@
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando...
{:else}
{isEdit ? 'Actualizar' : 'Crear'}

View File

@@ -3,6 +3,7 @@
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { classesApi, type A76Class } from "$lib/api/dashboard/a76/classes";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
@@ -80,21 +81,7 @@
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="5" r="1" />
<circle cx="12" cy="19" r="1" />
</svg>
<EllipsisVertical size={16} />
</Button>
{/snippet}
</DropdownMenu.Trigger>
@@ -102,63 +89,15 @@
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
<path d="m15 5 4 4" />
</svg>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>

View File

@@ -56,10 +56,10 @@ export function createColumns(onSuccess?: () => void): ColumnDef<ClientProvider>
const typeSnippet = createRawSnippet<[{ type: string | null | undefined }]>((getType) => {
const { type } = getType();
const displayType = type === 'client' ? 'Cliente' : type === 'provider' ? 'Proveedor' : type === 'both' ? 'Ambos' : 'N/A';
const colorClass = type === 'client' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
const colorClass = type === 'client' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
: type === 'provider' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
: type === 'both' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300'
: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
: type === 'both' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300'
: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
return {
render: () => `<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">${displayType}</span>`
};
@@ -81,21 +81,21 @@ export function createColumns(onSuccess?: () => void): ColumnDef<ClientProvider>
}
},
{
accessorKey: "enabled_disabled",
accessorKey: "is_active",
header: "Estado",
cell: ({ row }) => {
const statusSnippet = createRawSnippet<[{ status: number | undefined }]>((getStatus) => {
const { status } = getStatus();
const isEnabled = status === 1;
const statusText = isEnabled ? 'Activo' : 'Inactivo';
const colorClass = isEnabled
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
const colorClass = isEnabled
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
return {
render: () => `<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">${statusText}</span>`
};
});
return renderSnippet(statusSnippet, { status: row.original.enabled_disabled });
return renderSnippet(statusSnippet, { status: row.original.is_active });
}
},
{

View File

@@ -5,6 +5,7 @@
import { Label } from "$lib/components/ui/label";
import { clientsProvidersApi, type ClientProvider, type CreateClientProviderData, type UpdateClientProviderData } from "$lib/api/dashboard/a76/clients-providers";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -24,7 +25,7 @@
domicile_fiscal: "",
foreign_tax_id: "",
client_or_provider: "client",
enabled_disabled: 1,
is_active: true,
// Address fields
street: "",
neighborhood: "",
@@ -51,7 +52,7 @@
domicile_fiscal: item.domicile_fiscal || "",
foreign_tax_id: item.foreign_tax_id || "",
client_or_provider: item.client_or_provider || "client",
enabled_disabled: item.enabled_disabled ?? 1,
is_active: item.is_active ?? true,
street: item.address?.street || "",
neighborhood: item.address?.neighborhood || "",
city: item.address?.city || "",
@@ -70,7 +71,7 @@
domicile_fiscal: "",
foreign_tax_id: "",
client_or_provider: "client",
enabled_disabled: 1,
is_active: true,
street: "",
neighborhood: "",
city: "",
@@ -107,7 +108,7 @@
domicile_fiscal: formData.domicile_fiscal || null,
foreign_tax_id: formData.foreign_tax_id || null,
client_or_provider: formData.client_or_provider as "client" | "provider" | "both" | null,
enabled_disabled: formData.enabled_disabled,
is_active: formData.is_active,
address: {
street: formData.street || null,
neighborhood: formData.neighborhood || null,
@@ -132,7 +133,7 @@
domicile_fiscal: formData.domicile_fiscal || null,
foreign_tax_id: formData.foreign_tax_id || null,
client_or_provider: formData.client_or_provider as "client" | "provider" | "both" | null,
enabled_disabled: formData.enabled_disabled,
is_active: formData.is_active,
address: {
street: formData.street || null,
neighborhood: formData.neighborhood || null,
@@ -185,7 +186,7 @@
domicile_fiscal: "",
foreign_tax_id: "",
client_or_provider: "client",
enabled_disabled: 1,
is_active: true,
street: "",
neighborhood: "",
city: "",
@@ -432,26 +433,7 @@
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
{isEditing ? "Guardar cambios" : "Crear"}
</Button>

View File

@@ -90,7 +90,7 @@
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleToggleStatus} disabled={isToggling}>
{isToggling ? 'Cambiando...' : item.enabled_disabled === 1 ? 'Desactivar' : 'Activar'}
{isToggling ? 'Cambiando...' : item.is_active === true ? 'Desactivar' : 'Activar'}
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>

View File

@@ -3,6 +3,7 @@
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { clientsProvidersApi, type ClientProvider } from "$lib/api/dashboard/a76/clients-providers";
import { companyStore } from "$lib/stores/company.svelte";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -97,26 +98,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -85,7 +85,7 @@
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Estado</span>
{#if item.enabled_disabled === 1}
{#if item.is_active === true}
<span class="inline-flex w-fit items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-300">
Activo
</span>

View File

@@ -5,6 +5,7 @@
import type { CustomsBroker } from "./columns.js";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
import EditDialog from "./edit-dialog.svelte";
let {
broker,
@@ -16,6 +17,7 @@
let showDetailsDialog = $state(false);
let showDeleteDialog = $state(false);
let showEditDialog = $state(false);
function handleCopyKey() {
navigator.clipboard.writeText(broker.broker_key);
@@ -31,6 +33,10 @@
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
@@ -64,6 +70,9 @@
<DropdownMenu.Item onclick={handleViewDetails}>
Ver detalles
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-destructive">
Eliminar
</DropdownMenu.Item>
@@ -72,4 +81,5 @@
</DropdownMenu.Root>
<DetailsDialog bind:open={showDetailsDialog} {broker} />
<EditDialog bind:open={showEditDialog} {broker} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {broker} {onSuccess} />

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { customsBrokersApi, type CustomsBroker } from "$lib/api/dashboard/a76/customs-brokers";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -92,26 +93,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Eliminando...
{:else}
Eliminar

View File

@@ -0,0 +1,384 @@
<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 { customsBrokersApi, type CreateCustomsBrokerData, type CustomsBroker } from "$lib/api/dashboard/a76/customs-brokers";
import { companyStore } from "$lib/stores/company.svelte";
let {
open = $bindable(false),
broker,
onSuccess
}: {
open: boolean;
broker: CustomsBroker;
onSuccess?: () => void;
} = $props();
let formData = $state({
name: "",
type: "",
address: "",
postal_code: "",
city: "",
state: "",
phone: "",
fax: "",
email: "",
country: "",
tax_id: "",
personal_id: "",
position: "",
license: "",
company: "",
contact: ""
});
let loading = $state(false);
let error = $state<string | null>(null);
// Inicializar formulario cuando cambia el broker
$effect(() => {
if (open && broker) {
formData = {
name: broker.name || "",
type: broker.type || "",
address: broker.address || "",
postal_code: broker.postal_code || "",
city: broker.city || "",
state: broker.state || "",
phone: broker.phone || "",
fax: broker.fax || "",
email: broker.email || "",
country: broker.country || "",
tax_id: broker.tax_id || "",
personal_id: broker.personal_id || "",
position: broker.position || "",
license: broker.license || "",
company: broker.company || "",
contact: broker.contact || ""
};
}
});
async function handleSubmit(e: Event) {
e.preventDefault();
if (!companyStore.activeCompany) {
error = "No hay compañía seleccionada";
return;
}
loading = true;
error = null;
try {
const payload: CreateCustomsBrokerData = {
broker_key: broker.broker_key, // La clave no se edita
name: formData.name || null,
type: formData.type || null,
address: formData.address || null,
postal_code: formData.postal_code || null,
city: formData.city || null,
state: formData.state || null,
phone: formData.phone || null,
fax: formData.fax || null,
email: formData.email || null,
country: formData.country || null,
tax_id: formData.tax_id || null,
personal_id: formData.personal_id || null,
position: formData.position || null,
license: formData.license || null,
company: formData.company || null,
contact: formData.contact || null,
tenant_id: broker.tenant_id,
company_id: companyStore.activeCompany.id.toString()
};
const response = await customsBrokersApi.update(broker.broker_key, payload);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = response.error;
}
return;
}
// Éxito
open = false;
if (onSuccess) {
onSuccess();
}
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error updating:", e);
} finally {
loading = false;
}
}
function handleOpenChange(newOpen: boolean) {
open = newOpen;
if (!newOpen) {
error = null;
}
}
</script>
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<Dialog.Header>
<Dialog.Title>Editar Agente Aduanal</Dialog.Title>
<Dialog.Description>
Modifica los datos del agente aduanal <span class="font-mono font-semibold">{broker?.broker_key}</span>.
</Dialog.Description>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-6">
{#if error}
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Información básica -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Básica</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-broker_key">Clave</Label>
<Input
id="edit-broker_key"
value={broker?.broker_key}
disabled
class="bg-muted"
/>
</div>
<div class="space-y-2">
<Label for="edit-type">Tipo</Label>
<Input
id="edit-type"
bind:value={formData.type}
placeholder="Tipo de agente"
maxlength={9}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-name">Nombre</Label>
<Input
id="edit-name"
bind:value={formData.name}
placeholder="Nombre del agente aduanal"
maxlength={80}
disabled={loading}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-license">Patente</Label>
<Input
id="edit-license"
bind:value={formData.license}
placeholder="Número de patente"
maxlength={4}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-company">Empresa</Label>
<Input
id="edit-company"
bind:value={formData.company}
placeholder="Empresa del agente"
maxlength={200}
disabled={loading}
/>
</div>
</div>
</div>
<!-- Información de contacto -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información de Contacto</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-phone">Teléfono</Label>
<Input
id="edit-phone"
bind:value={formData.phone}
placeholder="Número telefónico"
maxlength={30}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-fax">Fax</Label>
<Input
id="edit-fax"
bind:value={formData.fax}
placeholder="Número de fax"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-email">Email</Label>
<Input
id="edit-email"
type="email"
bind:value={formData.email}
placeholder="correo@ejemplo.com"
maxlength={100}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-contact">Contacto</Label>
<Input
id="edit-contact"
bind:value={formData.contact}
placeholder="Nombre del contacto"
maxlength={80}
disabled={loading}
/>
</div>
</div>
<!-- Dirección -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Dirección</h3>
<div class="space-y-2">
<Label for="edit-address">Dirección</Label>
<Input
id="edit-address"
bind:value={formData.address}
placeholder="Calle y número"
maxlength={1500}
disabled={loading}
/>
</div>
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="edit-postal_code">Código Postal</Label>
<Input
id="edit-postal_code"
bind:value={formData.postal_code}
placeholder="C.P."
maxlength={15}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-city">Ciudad</Label>
<Input
id="edit-city"
bind:value={formData.city}
placeholder="Ciudad"
maxlength={30}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-state">Estado</Label>
<Input
id="edit-state"
bind:value={formData.state}
placeholder="Estado"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-country">País</Label>
<Input
id="edit-country"
bind:value={formData.country}
placeholder="País"
maxlength={3}
disabled={loading}
/>
</div>
</div>
<!-- Información fiscal -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Fiscal</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-tax_id">RFC</Label>
<Input
id="edit-tax_id"
bind:value={formData.tax_id}
placeholder="RFC"
maxlength={30}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="edit-personal_id">CURP</Label>
<Input
id="edit-personal_id"
bind:value={formData.personal_id}
placeholder="CURP"
maxlength={20}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-position">Posición</Label>
<Input
id="edit-position"
bind:value={formData.position}
placeholder="Cargo o posición"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<div class="flex items-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"></div>
Guardando...
</div>
{:else}
Guardar Cambios
{/if}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { pedimentosApi, type Pedimento } from "$lib/api/dashboard/a76/pedimentos";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
let {
item,
@@ -62,21 +63,7 @@
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="5" r="1" />
<circle cx="12" cy="19" r="1" />
</svg>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
@@ -84,63 +71,15 @@
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
<path d="m15 5 4 4" />
</svg>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>

View File

@@ -1,90 +1,40 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoDatesApi, type PedimentoDates } from '$lib/api/dashboard/a76/pedimento-dates';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
pedimentoId,
pedimento,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
pedimento: Pedimento | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadDates();
});
async function loadDates() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
// Inicializar formData inmediatamente
const datesData = pedimento?.pedimento_dates;
if (datesData) {
exists = true;
if (!formData) {
formData = {
entry_date: '',
pedimento_date: '',
payment_date: '',
rectification_payment_date: '',
extraction_date: '',
submission_date: '',
eucan_date: '',
original_date: '',
start_date: '',
end_date: '',
capture_date: '',
capture_time: ''
entry_date: datesData.entry_date ? datesData.entry_date.substring(0, 10) : '',
pedimento_date: datesData.pedimento_date ? datesData.pedimento_date.substring(0, 10) : '',
payment_date: datesData.payment_date ? datesData.payment_date.substring(0, 10) : '',
rectification_payment_date: datesData.rectification_payment_date ? datesData.rectification_payment_date.substring(0, 10) : '',
extraction_date: datesData.extraction_date ? datesData.extraction_date.substring(0, 10) : '',
submission_date: datesData.submission_date ? datesData.submission_date.substring(0, 10) : '',
eucan_date: datesData.eucan_date ? datesData.eucan_date.substring(0, 10) : '',
original_date: datesData.original_date ? datesData.original_date.substring(0, 10) : '',
start_date: datesData.start_date ? datesData.start_date.substring(0, 10) : '',
end_date: datesData.end_date ? datesData.end_date.substring(0, 10) : '',
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoDatesApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
entry_date: '',
pedimento_date: '',
payment_date: '',
rectification_payment_date: '',
extraction_date: '',
submission_date: '',
eucan_date: '',
original_date: '',
start_date: '',
end_date: '',
capture_date: '',
capture_time: ''
};
} else if (response.data) {
exists = true;
formData = {
entry_date: response.data.entry_date ? response.data.entry_date.substring(0, 10) : '',
pedimento_date: response.data.pedimento_date ? response.data.pedimento_date.substring(0, 10) : '',
payment_date: response.data.payment_date ? response.data.payment_date.substring(0, 10) : '',
rectification_payment_date: response.data.rectification_payment_date ? response.data.rectification_payment_date.substring(0, 10) : '',
extraction_date: response.data.extraction_date ? response.data.extraction_date.substring(0, 10) : '',
submission_date: response.data.submission_date ? response.data.submission_date.substring(0, 10) : '',
eucan_date: response.data.eucan_date ? response.data.eucan_date.substring(0, 10) : '',
original_date: response.data.original_date ? response.data.original_date.substring(0, 10) : '',
start_date: response.data.start_date ? response.data.start_date.substring(0, 10) : '',
end_date: response.data.end_date ? response.data.end_date.substring(0, 10) : '',
capture_date: response.data.capture_date ? response.data.capture_date.substring(0, 10) : '',
capture_time: response.data.capture_time || ''
};
}
} catch (e) {
console.error('Error loading dates:', e);
exists = false;
} else {
exists = false;
if (!formData) {
formData = {
entry_date: '',
pedimento_date: '',
@@ -96,11 +46,8 @@
original_date: '',
start_date: '',
end_date: '',
capture_date: '',
capture_time: ''
};
} finally {
loading = false;
}
}
</script>
@@ -113,15 +60,8 @@
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Fecha de Entrada -->
<div class="space-y-2">
<Label for="entry_date">Fecha de Entrada</Label>
@@ -220,29 +160,8 @@
type="date"
bind:value={formData.end_date}
/>
</div>
<!-- Fecha de Captura -->
<div class="space-y-2">
<Label for="capture_date">Fecha de Captura</Label>
<Input
id="capture_date"
type="date"
bind:value={formData.capture_date}
/>
</div>
<!-- Hora de Captura -->
<div class="space-y-2">
<Label for="capture_time">Hora de Captura</Label>
<Input
id="capture_time"
type="time"
bind:value={formData.capture_time}
/>
</div>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>
</Card.Content>
</Card.Root>

View File

@@ -2,20 +2,178 @@
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
import type { PedimentoCode } from '$lib/api/dashboard/refrence_data/pedimento_codes';
import type { CustomsSection } from '$lib/api/dashboard/refrence_data/customs_sections';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import type { CodePedimentoRegimen } from '$lib/api/dashboard/refrence_data/code_pedimento_regimens';
let {
pedimento,
formData = $bindable()
formData = $bindable(),
pedimentoCodes = [],
customsSections = [],
customsBrokers = [],
clients = [],
codePedimentoRegimens = []
}: {
pedimento: Pedimento | null;
formData?: any;
pedimentoCodes?: PedimentoCode[];
customsSections?: CustomsSection[];
customsBrokers?: CustomsBroker[];
clients?: ClientProvider[];
codePedimentoRegimens?: CodePedimentoRegimen[];
} = $props();
// Extraer regímenes únicos de codePedimentoRegimens
const uniqueRegimens = $derived(
Array.from(new Set(codePedimentoRegimens.map(r => r.regimen_code).filter((code): code is string => code !== null)))
.sort()
.map(code => ({ code, label: code }))
);
// Funciones de mapeo entre type_code (E/I) y operation_type (1/2)
function typeCodeToOperationType(typeCode: string | null | undefined): number | null {
if (!typeCode) return null;
// E = Exportación = 1, I = Importación = 2
if (typeCode.toUpperCase() === 'E') return 1;
if (typeCode.toUpperCase() === 'I') return 2;
return null;
}
function operationTypeToTypeCode(operationType: number | null | undefined): string | null {
if (operationType === null || operationType === undefined) return null;
// 1 = Exportación = E, 2 = Importación = I
if (operationType === 1) return 'E';
if (operationType === 2) return 'I';
return null;
}
// Opciones filtradas para Régimen y Tipo de Operación basadas en las selecciones actuales
// NOTA: La Clave NO se filtra, siempre muestra todas las opciones
const filteredRegimens = $derived(() => {
// Si hay clave o tipo de operación seleccionado, filtrar
if (formData.pedimento_code || formData.operation_type !== null) {
const matches = codePedimentoRegimens.filter(r => {
const matchesCode = !formData.pedimento_code || r.pedimento_code === formData.pedimento_code;
const matchesType = formData.operation_type === null || r.type_code === operationTypeToTypeCode(formData.operation_type);
return matchesCode && matchesType;
});
const validRegimens = new Set(matches.map(m => m.regimen_code).filter((code): code is string => code !== null));
return Array.from(validRegimens).sort().map(code => ({ code, label: code }));
}
return uniqueRegimens;
});
const filteredOperationTypes = $derived(() => {
// Si hay clave o régimen seleccionado, filtrar
if (formData.pedimento_code || formData.regime) {
const matches = codePedimentoRegimens.filter(r => {
const matchesCode = !formData.pedimento_code || r.pedimento_code === formData.pedimento_code;
const matchesRegime = !formData.regime || r.regimen_code === formData.regime;
return matchesCode && matchesRegime;
});
const validTypes = new Set(matches.map(m => typeCodeToOperationType(m.type_code)).filter(t => t !== null));
return operationOptions.filter(opt => validTypes.has(opt.value));
}
return operationOptions;
});
// Reactive synchronization between Clave, Régimen, and Tipo de Operación
// REGLA: La Clave es el campo principal y NUNCA se modifica automáticamente
// Solo se auto-llenan Régimen y Tipo de Operación basándose en la Clave
// Cuando cambia la Clave del Pedimento
$effect(() => {
const currentCode = formData.pedimento_code;
if (!currentCode) return;
const matches = codePedimentoRegimens.filter(r => r.pedimento_code === currentCode);
if (matches.length === 0) return;
// Verificar si los valores actuales de régimen y tipo son válidos para esta clave
const currentIsValid = matches.some(m => {
const matchesRegime = !formData.regime || m.regimen_code === formData.regime;
const matchesType = formData.operation_type === null || m.type_code === operationTypeToTypeCode(formData.operation_type);
return matchesRegime && matchesType;
});
// Si los valores actuales son válidos, NO auto-llenar
if (currentIsValid && (formData.regime || formData.operation_type !== null)) {
return;
}
// Si solo hay un match y no hay valores válidos, auto-llenar
if (matches.length === 1) {
const match = matches[0];
if (match.regimen_code && formData.regime !== match.regimen_code) {
formData.regime = match.regimen_code;
}
const expectedOpType = typeCodeToOperationType(match.type_code);
if (expectedOpType !== null && formData.operation_type !== expectedOpType) {
formData.operation_type = expectedOpType;
}
}
});
// Cuando cambia el Régimen
$effect(() => {
const currentRegime = formData.regime;
if (!currentRegime) return;
const matches = codePedimentoRegimens.filter(r => r.regimen_code === currentRegime);
if (matches.length === 0) return;
// Si hay clave seleccionada, solo validar (NO auto-llenar tipo de operación)
if (formData.pedimento_code) {
const exactMatch = matches.find(m => m.pedimento_code === formData.pedimento_code);
}
// Si hay tipo de operación pero no clave, no hacer nada
// (el usuario debe seleccionar la clave primero)
});
// Cuando cambia el Tipo de Operación
$effect(() => {
const currentType = formData.operation_type;
if (currentType === null || currentType === undefined) return;
const expectedTypeCode = operationTypeToTypeCode(currentType);
const matches = codePedimentoRegimens.filter(r => r.type_code === expectedTypeCode);
if (matches.length === 0) return;
// Si hay clave seleccionada, actualizar régimen (forzar si no hay match exacto)
if (formData.pedimento_code) {
const exactMatch = matches.find(m => m.pedimento_code === formData.pedimento_code);
if (exactMatch?.regimen_code && formData.regime !== exactMatch.regimen_code) {
formData.regime = exactMatch.regimen_code;
} else if (!exactMatch) {
// No hay match exacto - buscar cualquier match con la clave actual
const allMatchesForClave = codePedimentoRegimens.filter(r => r.pedimento_code === formData.pedimento_code);
if (allMatchesForClave.length > 0) {
// Forzar el régimen al primer match disponible para esta clave
const firstMatch = allMatchesForClave[0];
if (firstMatch.regimen_code) formData.regime = firstMatch.regimen_code;
}
}
}
// Si hay régimen pero no clave, no hacer nada
// (el usuario debe seleccionar la clave primero)
});
// Obtener el año actual (últimos 2 dígitos)
const currentYear = String(new Date().getFullYear()).slice(-2);
// Inicializar formData con los valores del pedimento (o vacío si es null)
if (!formData) {
formData = {
year: pedimento?.year || '',
year: pedimento?.year || currentYear,
customs_office: pedimento?.customs_office || '',
license: pedimento?.license || '',
pedimento_number: pedimento?.pedimento_number || '',
@@ -32,6 +190,18 @@
};
}
// Asegurar que el año siempre esté actualizado con el año actual
$effect(() => {
if (formData && !pedimento?.year) {
formData.year = currentYear;
}
});
const operationOptions = [
{ value: 1, label: 'Exportación' },
{ value: 2, label: 'Importación' },
];
const statusOptions = [
{ value: 'MODIFICABLE', label: 'Modificable' },
{ value: 'ESPERA FIRMA PREVIO', label: 'Espera de firma previo' },
@@ -71,6 +241,8 @@
placeholder="23"
maxlength={2}
class="text-center"
disabled
readonly
/>
</div>
@@ -78,15 +250,28 @@
<div class="pb-2 text-2xl font-semibold text-muted-foreground">-</div>
<!-- Aduana -->
<div class="space-y-2 w-16">
<div class="space-y-2 w-20">
<Label for="customs_office">Aduana</Label>
<Input
id="customs_office"
bind:value={formData.customs_office}
placeholder="01"
maxlength={2}
class="text-center"
/>
<Select.Root
type="single"
value={formData.customs_office || ''}
onValueChange={(v: string) => formData.customs_office = v ?? ''}
>
<Select.Trigger class="w-full">
<span class="truncate">
{formData.customs_office || 'Sel...'}
</span>
</Select.Trigger>
<Select.Content class="max-w-[300px] max-h-[300px]">
{#each customsSections as section}
<Select.Item value={section.customs_code}>
<span class="truncate overflow-hidden text-ellipsis whitespace-nowrap" title={`${section.customs_code} - ${section.section_name}`}>
{section.customs_code} - {section.section_name}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Separador -->
@@ -95,13 +280,26 @@
<!-- Patente -->
<div class="space-y-2 w-24">
<Label for="license">Patente</Label>
<Input
id="license"
bind:value={formData.license}
placeholder="1234"
maxlength={4}
class="text-center"
/>
<Select.Root
type="single"
value={formData.license || ''}
onValueChange={(v: string) => formData.license = v ?? ''}
>
<Select.Trigger class="w-full">
<span class="truncate">
{formData.license || 'Sel...'}
</span>
</Select.Trigger>
<Select.Content class="max-w-[300px] max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.license}>
<span class="truncate overflow-hidden text-ellipsis whitespace-nowrap" title={`${broker.broker_key} - ${broker.name || ''}`}>
{broker.broker_key} - {broker.name || ''}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Separador -->
@@ -123,24 +321,27 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- ID del Cliente -->
<div class="space-y-2">
<Label for="client_id">ID del Cliente</Label>
<Input
id="client_id"
type="number"
bind:value={formData.client_id}
placeholder="Ej: 123"
/>
</div>
<!-- Tipo de Operación -->
<div class="space-y-2">
<Label for="operation_type">Tipo de Operación</Label>
<Input
id="operation_type"
type="number"
bind:value={formData.operation_type}
placeholder="Ej: 1"
/>
<Label for="client_id">Cliente</Label>
<Select.Root
type="single"
value={String(formData.client_id ?? '')}
onValueChange={(v: string) => formData.client_id = v ? Number(v) : null}
>
<Select.Trigger class="w-full">
<span class="truncate">
{clients.find(c => c.id === formData.client_id)?.name || 'Seleccionar cliente...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each clients as client}
<Select.Item value={String(client.id)} label={client.name}>
<span class="truncate overflow-hidden text-ellipsis whitespace-nowrap" title={client.name}>
{client.name}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Tipo de Pedimento -->
@@ -154,39 +355,99 @@
/>
</div>
<!-- Clave del Pedimento -->
<div class="space-y-2">
<Label for="pedimento_code">Clave del Pedimento</Label>
<Input
id="pedimento_code"
bind:value={formData.pedimento_code}
placeholder="Ej: A1"
/>
</div>
</div>
<!-- Régimen -->
<div class="space-y-2">
<Label for="regime">Régimen</Label>
<Input
id="regime"
bind:value={formData.regime}
placeholder="Ej: IMD"
/>
</div>
<!-- Fila: Clave (2), Régimen (3), Tipo de Operación (11) -->
<div class="flex items-end gap-2">
<!-- Clave del Pedimento -->
<div class="space-y-2 w-20">
<Label for="pedimento_code">Clave</Label>
<Select.Root
type="single"
value={formData.pedimento_code || ''}
onValueChange={(v: string) => formData.pedimento_code = v ?? ''}
>
<Select.Trigger class="w-full">
<span class="truncate">
{pedimentoCodes.find(o => o.code === formData.pedimento_code)?.code || 'Sel...'}
</span>
</Select.Trigger>
<Select.Content class="max-w-[200px]">
{#each pedimentoCodes as code}
<Select.Item value={code.code}>
<span class="truncate overflow-hidden text-ellipsis whitespace-nowrap" title={`${code.code} - ${code.description}`}>
{code.code} - {code.description}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Régimen -->
<div class="space-y-2 w-24">
<Label for="regime">Régimen</Label>
<Select.Root
type="single"
value={formData.regime || ''}
onValueChange={(v: string) => formData.regime = v ?? ''}
>
<Select.Trigger class="w-full">
<span class="truncate">
{formData.regime || 'Sel...'}
</span>
</Select.Trigger>
<Select.Content class="max-w-[200px]">
{#each filteredRegimens() as regimen}
<Select.Item value={regimen.code}>
<span class="truncate overflow-hidden text-ellipsis whitespace-nowrap" title={regimen.code}>
{regimen.code}
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Tipo de Operación -->
<div class="space-y-2 flex-1">
<Label for="operation_type">Tipo de Operación</Label>
<Select.Root
type="single"
value={String(formData.operation_type ?? '')}
onValueChange={(v: string) => formData.operation_type = v ? Number(v) : null}
>
<Select.Trigger class="w-full">
{operationOptions.find(o => o.value === formData.operation_type)?.label || 'Seleccionar...'}
</Select.Trigger>
<Select.Content>
{#each filteredOperationTypes() as option}
<Select.Item value={String(option.value)} label={option.label} />
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Estado -->
<div class="space-y-2">
<Label for="status">Estado</Label>
<select
id="status"
bind:value={formData.status}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
<Select.Root
type="single"
value={formData.status || ''}
onValueChange={(v: string) => formData.status = v ?? ''}
>
<option value="">Seleccionar...</option>
{#each statusOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<Select.Trigger class="w-full">
{statusOptions.find(o => o.value === formData.status)?.label || 'Seleccionar...'}
</Select.Trigger>
<Select.Content>
{#each statusOptions as option}
<Select.Item value={option.value} label={option.label} />
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Valor USD -->

View File

@@ -1,106 +1,54 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoPaymentsApi } from '$lib/api/dashboard/a76/pedimento-payments';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
pedimentoId,
pedimento,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
pedimento: Pedimento | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadPayments();
});
async function loadPayments() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
// Inicializar formData inmediatamente
const paymentsData = pedimento?.pedimento_payments;
if (paymentsData) {
exists = true;
if (!formData) {
formData = {
acknowledgment: '',
operation_number: '',
bank_code: null,
cashier: '',
date: '',
time: '',
shift: '',
total_cash_paid: null,
total_contributions: null,
counter_payment: null,
pece_code: '',
payment_id: null
acknowledgment: paymentsData.acknowledgment || '',
operation_number: paymentsData.operation_number || '',
bank_code: paymentsData.bank_code || '',
cashier: paymentsData.cashier || '',
date: paymentsData.date ? paymentsData.date.substring(0, 10) : '',
time: paymentsData.time || '',
shift: paymentsData.shift || '',
total_cash_paid: paymentsData.total_cash_paid || '',
total_contributions: paymentsData.total_contributions || '',
counter_payment: paymentsData.counter_payment || '',
pece_code: paymentsData.pece_code || '',
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoPaymentsApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
acknowledgment: '',
operation_number: '',
bank_code: null,
cashier: '',
date: '',
time: '',
shift: '',
total_cash_paid: null,
total_contributions: null,
counter_payment: null,
pece_code: '',
payment_id: null
};
} else if (response.data) {
exists = true;
formData = {
acknowledgment: response.data.acknowledgment || '',
operation_number: response.data.operation_number || '',
bank_code: response.data.bank_code || null,
cashier: response.data.cashier || '',
date: response.data.date || '',
time: response.data.time || '',
shift: response.data.shift || '',
total_cash_paid: response.data.total_cash_paid || null,
total_contributions: response.data.total_contributions || null,
counter_payment: response.data.counter_payment || null,
pece_code: response.data.pece_code || '',
payment_id: response.data.payment_id || null
};
}
} catch (e) {
console.error('Error loading payments:', e);
exists = false;
} else {
exists = false;
if (!formData) {
formData = {
acknowledgment: '',
operation_number: '',
bank_code: null,
bank_code: '',
cashier: '',
date: '',
time: '',
shift: '',
total_cash_paid: null,
total_contributions: null,
counter_payment: null,
pece_code: '',
payment_id: null
total_cash_paid: '',
total_contributions: '',
counter_payment: '',
pece_code: '',
};
} finally {
loading = false;
}
}
</script>
@@ -113,145 +61,128 @@
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Acuse -->
<div class="space-y-2">
<Label for="acknowledgment">Acuse</Label>
<Input
id="acknowledgment"
bind:value={formData.acknowledgment}
placeholder="Máx. 20 caracteres"
maxlength={20}
/>
</div>
<!-- Número de Operación -->
<div class="space-y-2">
<Label for="operation_number">Número de Operación</Label>
<Input
id="operation_number"
bind:value={formData.operation_number}
placeholder="Máx. 14 caracteres"
maxlength={14}
/>
</div>
<!-- Código de Banco -->
<div class="space-y-2">
<Label for="bank_code">Código de Banco</Label>
<Input
id="bank_code"
type="number"
bind:value={formData.bank_code}
placeholder="Código numérico"
/>
</div>
<!-- Cajero -->
<div class="space-y-2">
<Label for="cashier">Cajero</Label>
<Input
id="cashier"
bind:value={formData.cashier}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Fecha -->
<div class="space-y-2">
<Label for="date">Fecha</Label>
<Input
id="date"
type="date"
bind:value={formData.date}
/>
</div>
<!-- Hora -->
<div class="space-y-2">
<Label for="time">Hora</Label>
<Input
id="time"
type="time"
bind:value={formData.time}
/>
</div>
<!-- Turno -->
<div class="space-y-2">
<Label for="shift">Turno</Label>
<Input
id="shift"
bind:value={formData.shift}
placeholder="1 carácter"
maxlength={1}
/>
</div>
<!-- Total Efectivo Pagado -->
<div class="space-y-2">
<Label for="total_cash_paid">Total Efectivo Pagado</Label>
<Input
id="total_cash_paid"
type="number"
bind:value={formData.total_cash_paid}
placeholder="Monto en efectivo"
/>
</div>
<!-- Total Contribuciones -->
<div class="space-y-2">
<Label for="total_contributions">Total Contribuciones</Label>
<Input
id="total_contributions"
type="number"
bind:value={formData.total_contributions}
placeholder="Monto total"
/>
</div>
<!-- Pago en Ventanilla -->
<div class="space-y-2">
<Label for="counter_payment">Pago en Ventanilla</Label>
<Input
id="counter_payment"
type="number"
bind:value={formData.counter_payment}
placeholder="Monto"
/>
</div>
<!-- Código PECE -->
<div class="space-y-2">
<Label for="pece_code">Código PECE</Label>
<Input
id="pece_code"
bind:value={formData.pece_code}
placeholder="Máx. 5 caracteres"
maxlength={5}
/>
</div>
<!-- ID de Pago -->
<div class="space-y-2">
<Label for="payment_id">ID de Pago</Label>
<Input
id="payment_id"
type="number"
bind:value={formData.payment_id}
placeholder="Identificador"
/>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Acuse -->
<div class="space-y-2">
<Label for="acknowledgment">Acuse</Label>
<Input
id="acknowledgment"
bind:value={formData.acknowledgment}
maxlength={20}
placeholder="Acuse"
/>
</div>
<!-- Número de Operación -->
<div class="space-y-2">
<Label for="operation_number">Número de Operación</Label>
<Input
id="operation_number"
bind:value={formData.operation_number}
maxlength={14}
placeholder="Número de operación"
/>
</div>
<!-- Código Bancario -->
<div class="space-y-2">
<Label for="bank_code">Código Bancario</Label>
<Input
id="bank_code"
type="number"
bind:value={formData.bank_code}
placeholder="Código del banco"
/>
</div>
<!-- Cajero -->
<div class="space-y-2">
<Label for="cashier">Cajero</Label>
<Input
id="cashier"
bind:value={formData.cashier}
maxlength={2}
placeholder="Cajero"
/>
</div>
<!-- Fecha -->
<div class="space-y-2">
<Label for="date">Fecha</Label>
<Input
id="date"
type="date"
bind:value={formData.date}
/>
</div>
<!-- Hora -->
<div class="space-y-2">
<Label for="time">Hora</Label>
<Input
id="time"
type="time"
bind:value={formData.time}
/>
</div>
<!-- Turno -->
<div class="space-y-2">
<Label for="shift">Turno</Label>
<Input
id="shift"
bind:value={formData.shift}
maxlength={1}
placeholder="Turno"
/>
</div>
<!-- Total Pagado en Efectivo -->
<div class="space-y-2">
<Label for="total_cash_paid">Total Pagado en Efectivo</Label>
<Input
id="total_cash_paid"
type="number"
bind:value={formData.total_cash_paid}
placeholder="Total en efectivo"
/>
</div>
<!-- Total Contribuciones -->
<div class="space-y-2">
<Label for="total_contributions">Total Contribuciones</Label>
<Input
id="total_contributions"
type="number"
bind:value={formData.total_contributions}
placeholder="Total contribuciones"
/>
</div>
<!-- Pago en Mostrador -->
<div class="space-y-2">
<Label for="counter_payment">Pago en Mostrador</Label>
<Input
id="counter_payment"
type="number"
bind:value={formData.counter_payment}
placeholder="Pago en mostrador"
/>
</div>
<!-- Código PECE -->
<div class="space-y-2">
<Label for="pece_code">Código PECE</Label>
<Input
id="pece_code"
bind:value={formData.pece_code}
maxlength={5}
placeholder="Código PECE"
/>
</div>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>

View File

@@ -1,74 +1,40 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoTransportApi } from '$lib/api/dashboard/a76/pedimento-transport';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
pedimentoId,
pedimento,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
pedimento: Pedimento | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadTransport();
});
async function loadTransport() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
// Inicializar formData inmediatamente
const transportData = pedimento?.pedimento_transport_means;
if (transportData) {
exists = true;
if (!formData) {
formData = {
destination: null,
entry_exit: '',
arrival: '',
departure: ''
destination: transportData.destination || '',
entry_exit: transportData.entry_exit || '',
arrival: transportData.arrival || '',
departure: transportData.departure || ''
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoTransportApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
destination: null,
entry_exit: '',
arrival: '',
departure: ''
};
} else if (response.data) {
exists = true;
formData = {
destination: response.data.destination || null,
entry_exit: response.data.entry_exit || '',
arrival: response.data.arrival || '',
departure: response.data.departure || ''
};
}
} catch (e) {
console.error('Error loading transport:', e);
exists = false;
} else {
exists = false;
if (!formData) {
formData = {
destination: null,
destination: '',
entry_exit: '',
arrival: '',
departure: ''
};
} finally {
loading = false;
}
}
</script>
@@ -81,61 +47,52 @@
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Destino -->
<div class="space-y-2">
<Label for="destination">Destino</Label>
<Input
id="destination"
type="number"
bind:value={formData.destination}
placeholder="Código de destino"
/>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Destino -->
<div class="space-y-2">
<Label for="destination">Destino</Label>
<Input
id="destination"
type="number"
bind:value={formData.destination}
placeholder="Destino"
/>
</div>
<!-- Entrada/Salida -->
<div class="space-y-2">
<Label for="entry_exit">Entrada/Salida</Label>
<Input
id="entry_exit"
bind:value={formData.entry_exit}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Entrada/Salida -->
<div class="space-y-2">
<Label for="entry_exit">Entrada/Salida</Label>
<Input
id="entry_exit"
bind:value={formData.entry_exit}
maxlength={2}
placeholder="Entrada/Salida"
/>
</div>
<!-- Llegada -->
<div class="space-y-2">
<Label for="arrival">Llegada</Label>
<Input
id="arrival"
bind:value={formData.arrival}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Llegada -->
<div class="space-y-2">
<Label for="arrival">Llegada</Label>
<Input
id="arrival"
bind:value={formData.arrival}
maxlength={2}
placeholder="Llegada"
/>
</div>
<!-- Salida -->
<div class="space-y-2">
<Label for="departure">Salida</Label>
<Input
id="departure"
bind:value={formData.departure}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Salida -->
<div class="space-y-2">
<Label for="departure">Salida</Label>
<Input
id="departure"
bind:value={formData.departure}
maxlength={2}
placeholder="Salida"
/>
</div>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>

View File

@@ -1,79 +1,39 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Textarea } from '$lib/components/ui/textarea';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoValidationApi } from '$lib/api/dashboard/a76/pedimento-validation';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
pedimentoId,
pedimento,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
pedimento: Pedimento | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadValidation();
});
async function loadValidation() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
// Inicializar formData inmediatamente
const validationData = pedimento?.pedimento_validation;
if (validationData) {
exists = true;
if (!formData) {
formData = {
validator: '',
validation_ack: '',
pre_ack: '',
line_signature: '',
electronic_signature: '',
certificate_number: '',
validator_id: null,
responsible_id: null
validator: validationData.validator || '',
validation_ack: validationData.validation_ack || '',
pre_ack: validationData.pre_ack || '',
line_signature: validationData.line_signature || '',
electronic_signature: validationData.electronic_signature || '',
certificate_number: validationData.certificate_number || '',
validator_id: validationData.validator_id || '',
responsible_id: validationData.responsible_id || ''
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoValidationApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
validator: '',
validation_ack: '',
pre_ack: '',
line_signature: '',
electronic_signature: '',
certificate_number: '',
validator_id: null,
responsible_id: null
};
} else if (response.data) {
exists = true;
formData = {
validator: response.data.validator || '',
validation_ack: response.data.validation_ack || '',
pre_ack: response.data.pre_ack || '',
line_signature: response.data.line_signature || '',
electronic_signature: response.data.electronic_signature || '',
certificate_number: response.data.certificate_number || '',
validator_id: response.data.validator_id || null,
responsible_id: response.data.responsible_id || null
};
}
} catch (e) {
console.error('Error loading validation:', e);
exists = false;
} else {
exists = false;
if (!formData) {
formData = {
validator: '',
validation_ack: '',
@@ -81,11 +41,9 @@
line_signature: '',
electronic_signature: '',
certificate_number: '',
validator_id: null,
responsible_id: null
validator_id: '',
responsible_id: ''
};
} finally {
loading = false;
}
}
</script>
@@ -98,110 +56,96 @@
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-32 w-full" />
<Skeleton class="h-32 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Validador -->
<div class="space-y-2">
<Label for="validator">Validador</Label>
<Input
id="validator"
bind:value={formData.validator}
placeholder="Máx. 3 caracteres"
maxlength={3}
/>
</div>
<!-- Acuse de Validación -->
<div class="space-y-2">
<Label for="validation_ack">Acuse de Validación</Label>
<Input
id="validation_ack"
bind:value={formData.validation_ack}
placeholder="Máx. 8 caracteres"
maxlength={8}
/>
</div>
<!-- Pre-acuse -->
<div class="space-y-2">
<Label for="pre_ack">Pre-acuse</Label>
<Input
id="pre_ack"
bind:value={formData.pre_ack}
placeholder="Máx. 8 caracteres"
maxlength={8}
/>
</div>
<!-- Número de Certificado -->
<div class="space-y-2">
<Label for="certificate_number">Número de Certificado</Label>
<Input
id="certificate_number"
bind:value={formData.certificate_number}
placeholder="Máx. 99 caracteres"
maxlength={99}
/>
</div>
<!-- ID de Validador -->
<div class="space-y-2">
<Label for="validator_id">ID de Validador</Label>
<Input
id="validator_id"
type="number"
bind:value={formData.validator_id}
placeholder="Identificador numérico"
/>
</div>
<!-- ID de Responsable -->
<div class="space-y-2">
<Label for="responsible_id">ID de Responsable</Label>
<Input
id="responsible_id"
type="number"
bind:value={formData.responsible_id}
placeholder="Identificador numérico"
/>
</div>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Validador -->
<div class="space-y-2">
<Label for="validator">Validador</Label>
<Input
id="validator"
bind:value={formData.validator}
maxlength={3}
placeholder="Validador"
/>
</div>
<!-- Firma de Línea -->
<!-- Acuse de Validación -->
<div class="space-y-2">
<Label for="line_signature">Firma de Línea</Label>
<Label for="validation_ack">Acuse de Validación</Label>
<Input
id="validation_ack"
bind:value={formData.validation_ack}
maxlength={8}
placeholder="Acuse de validación"
/>
</div>
<!-- Acuse Previo -->
<div class="space-y-2">
<Label for="pre_ack">Acuse Previo</Label>
<Input
id="pre_ack"
bind:value={formData.pre_ack}
maxlength={8}
placeholder="Acuse previo"
/>
</div>
<!-- Firma Línea de Captura -->
<div class="space-y-2">
<Label for="line_signature">Firma Línea de Captura</Label>
<Input
id="line_signature"
bind:value={formData.line_signature}
placeholder="Máx. 50 caracteres"
maxlength={50}
placeholder="Firma línea de captura"
/>
</div>
<!-- Firma Electrónica -->
<div class="space-y-2">
<div class="space-y-2 md:col-span-2">
<Label for="electronic_signature">Firma Electrónica</Label>
<Textarea
<Input
id="electronic_signature"
bind:value={formData.electronic_signature}
placeholder="Ingresa la firma electrónica..."
rows={6}
class="resize-none font-mono text-sm"
maxlength={999}
placeholder="Firma electrónica"
/>
</div>
<!-- Número de Certificado -->
<div class="space-y-2">
<Label for="certificate_number">Número de Certificado</Label>
<Input
id="certificate_number"
bind:value={formData.certificate_number}
maxlength={99}
placeholder="Número de certificado"
/>
</div>
<!-- ID de Validador -->
<div class="space-y-2">
<Label for="validator_id">ID de Validador</Label>
<Input
id="validator_id"
type="number"
bind:value={formData.validator_id}
placeholder="ID de validador"
/>
</div>
<!-- ID de Responsable -->
<div class="space-y-2">
<Label for="responsible_id">ID de Responsable</Label>
<Input
id="responsible_id"
type="number"
bind:value={formData.responsible_id}
placeholder="ID de responsable"
/>
<p class="text-sm text-muted-foreground">
Firma electrónica del pedimento (máximo 999 caracteres).
</p>
</div>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { codePedimentoRegimensApi, type CodePedimentoRegimen } from "$lib/api/dashboard/refrence_data/code_pedimento_regimens";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { containersApi, type Container } from "$lib/api/dashboard/refrence_data/containers";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -84,26 +85,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { customsSectionsApi, type CustomsSection } from "$lib/api/dashboard/refrence_data/customs_sections";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { customsWarehousesApi, type CustomsWarehouse } from "$lib/api/dashboard/refrence_data/customs_warehouses";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { materialTypesApi, type MaterialType } from "$lib/api/dashboard/refrence_data/material_types";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -88,26 +89,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { paymentMethodsApi, type PaymentMethod } from "$lib/api/dashboard/refrence_data/payment_methods";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -84,26 +85,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { pedimentoRegimensApi, type PedimentoRegimen } from "$lib/api/dashboard/refrence_data/pedimento_regimens";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -84,26 +85,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -4,6 +4,7 @@
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { statesApi, type State, type CreateStateData, type UpdateStateData } from "$lib/api/dashboard/refrence_data/states";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -195,26 +196,7 @@
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
{isEditing ? "Guardar cambios" : "Crear"}
</Button>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { statesApi, type State } from "$lib/api/dashboard/refrence_data/states";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -96,26 +97,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -2,6 +2,7 @@
import { Button } from "$lib/components/ui/button";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { transportModesApi, type TransportMode } from "$lib/api/dashboard/refrence_data/transport_modes";
import { LoaderCircle } from 'lucide-svelte';
let {
open = $bindable(false),
@@ -84,26 +85,7 @@
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>

View File

@@ -10,6 +10,7 @@
import { Input } from "$lib/components/ui/input/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
import { FileText, ShieldCheck } from 'lucide-svelte';
import type { HTMLAttributes } from "svelte/elements";
import { page } from '$app/state';
import { enhance } from '$app/forms';
@@ -203,14 +204,10 @@
<div class="relative w-full h-full flex flex-col items-center justify-center gap-8">
<!-- Ícono principal - Escudo con documento -->
<div class="relative">
<svg class="w-48 h-48 text-blue-600 dark:text-blue-400 opacity-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<FileText class="w-48 h-48 text-blue-600 dark:text-blue-400 opacity-90" strokeWidth={1.5} />
<!-- Escudo de protección superpuesto -->
<div class="absolute -bottom-2 -right-2 bg-white dark:bg-slate-800 rounded-full p-2 shadow-lg">
<svg class="w-12 h-12 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
<ShieldCheck class="w-12 h-12 text-green-600 dark:text-green-400" fill="currentColor" />
</div>
</div>

View File

@@ -69,7 +69,7 @@ class CompanyStore {
// Si no hay datos pre-cargados, hacer fetch (fallback)
this._loading = true;
try {
const response = await fetch('/api/company/my-companies');
const response = await fetch('/api/v1/a76/company/my-companies');
if (response.ok) {
const newCompanies = await response.json();

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import { FileText, LayoutGrid, Package } from 'lucide-svelte';
</script>
<div class="space-y-6">
@@ -52,78 +53,27 @@
</Card.Header>
<Card.Content>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<a
href="/dashboard/reference_data/code_pedimento_regimens"
class="flex flex-col items-center justify-center rounded-lg border p-6 hover:bg-accent transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-2"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" x2="8" y1="13" y2="13" />
<line x1="16" x2="8" y1="17" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<span class="font-medium">Código Pedimento - Regímenes</span>
<span class="text-xs text-muted-foreground">Gestionar relaciones</span>
</a>
<div
class="flex flex-col items-center justify-center rounded-lg border p-6 opacity-50 cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-2"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<line x1="3" x2="21" y1="9" y2="9" />
<line x1="9" x2="9" y1="21" y2="9" />
</svg>
<span class="font-medium">Catálogo de Tipos</span>
<span class="text-xs text-muted-foreground">Próximamente</span>
</div>
<div
class="flex flex-col items-center justify-center rounded-lg border p-6 opacity-50 cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mb-2"
>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" x2="12" y1="22.08" y2="12" />
</svg>
<span class="font-medium">Reportes</span>
<span class="text-xs text-muted-foreground">Próximamente</span>
</div>
<a
href="/dashboard/reference_data/code_pedimento_regimens"
class="flex flex-col items-center justify-center rounded-lg border p-6 hover:bg-accent transition-colors"
>
<FileText class="mb-2" size={24} />
<span class="font-medium">Código Pedimento - Regímenes</span>
<span class="text-xs text-muted-foreground">Gestionar relaciones</span>
</a> <div
class="flex flex-col items-center justify-center rounded-lg border p-6 opacity-50 cursor-not-allowed"
>
<LayoutGrid class="mb-2" size={24} />
<span class="font-medium">Catálogo de Tipos</span>
<span class="text-xs text-muted-foreground">Próximamente</span>
</div> <div
class="flex flex-col items-center justify-center rounded-lg border p-6 opacity-50 cursor-not-allowed"
>
<Package class="mb-2" size={24} />
<span class="font-medium">Reportes</span>
<span class="text-xs text-muted-foreground">Próximamente</span>
</div>
</Card.Content>
</Card.Root>
</div>
</div>
</Card.Content>
</Card.Root>
</div>

View File

@@ -4,9 +4,9 @@ import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return {
error: 'No authenticated',
@@ -22,20 +22,20 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
// Obtener parámetros de paginación de la URL
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No company selected',
@@ -47,9 +47,17 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
};
}
// Construir URL con parámetros
let apiUrl = `v1/a76/clients-providers?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
const type = url.searchParams.get('type');
if (type && type !== 'both') {
apiUrl += `&type=${type}`;
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/clients-providers?company_id=${companyId}&page=${page}&page_size=${pageSize}`,
apiUrl,
{},
cookies,
fetch
@@ -62,7 +70,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
statusText: response.statusText,
error: errorText
});
return {
error: `Error ${response.status}: ${response.statusText}`,
items: [],

View File

@@ -6,6 +6,10 @@
import CreateEditDialog from '$lib/components/dashboard/clients_and_providers/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { Plus, RefreshCw } from 'lucide-svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
@@ -16,6 +20,22 @@
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Estado para el filtro de tipo
let selectedType = $state<string>($page.url.searchParams.get('type') || 'both');
// Actualizar URL cuando cambia el filtro
function handleTypeChange(value: string) {
selectedType = value;
const url = new URL(window.location.href);
if (value === 'both') {
url.searchParams.delete('type');
} else {
url.searchParams.set('type', value);
}
goto(url.toString(), { keepFocus: true, noScroll: true, replaceState: true });
reloadData();
}
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
@@ -77,7 +97,8 @@
const response = await clientsProvidersApi.list(
companyStore.activeCompany.id,
currentPage + 1,
pageSize
pageSize,
selectedType !== 'both' ? { type: selectedType } : undefined
);
if (response.error) {
@@ -121,7 +142,8 @@
const response = await clientsProvidersApi.list(
companyStore.activeCompany.id,
1,
pageSize
pageSize,
selectedType !== 'both' ? { type: selectedType } : undefined
);
if (response.error) {
@@ -174,21 +196,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Cliente/Proveedor
</Button>
</div>
@@ -216,25 +224,23 @@
{/if}
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<div class="flex items-center gap-2">
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
<Select.Trigger class="w-[180px]">
{selectedType === 'both' ? 'Todos' : selectedType === 'client' ? 'Clientes' : 'Proveedores'}
</Select.Trigger>
<Select.Content>
<Select.Item value="both">Todos</Select.Item>
<Select.Item value="client">Clientes</Select.Item>
<Select.Item value="provider">Proveedores</Select.Item>
</Select.Content>
</Select.Root>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->

View File

@@ -11,6 +11,7 @@
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
import { Plus, Search, Trash2 } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -175,21 +176,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Agente Aduanal
</Button>
</div>
@@ -226,44 +213,15 @@
Buscando...
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<Search class="mr-2" size={16} />
Buscar
{/if}
</Button>
{#if searchedBroker}
<Button type="button" variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
Limpiar
</Button>
{#if searchedBroker}
<Button type="button" variant="outline" onclick={reloadData}>
<Trash2 class="mr-2" size={16} />
Limpiar
</Button>
{/if}
</div>
</div>

View File

@@ -9,6 +9,7 @@
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
import { browser } from '$app/environment';
// Estado para la lista de classes
@@ -255,21 +256,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nueva Clase
</Button>
</div>
@@ -305,38 +292,11 @@
<div class="flex items-end gap-2 md:col-span-2">
<Button type="submit" disabled={loading} class="flex-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
<Filter class="mr-2" size={16} />
Filtrar
</Button>
<Button type="button" variant="outline" onclick={clearFilters} disabled={loading}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 6h18" />
<path d="m19 6-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
<Trash2 size={16} />
</Button>
</div>
</form>
@@ -364,21 +324,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -10,6 +10,7 @@
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -260,21 +261,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Pedimento
</Button>
</div>
@@ -322,38 +309,11 @@
<div class="flex items-end gap-2">
<Button type="submit" disabled={loading} class="flex-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
<Filter class="mr-2" size={16} />
Filtrar
</Button>
<Button type="button" variant="outline" onclick={clearFilters} disabled={loading}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 6h18" />
<path d="m19 6-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
<Trash2 size={16} />
</Button>
</div>
</form>
@@ -381,21 +341,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -9,12 +9,78 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
throw redirect(302, '/login');
}
// Obtener el company_id de la cookie para ambas ramas (new y edit)
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
throw error(400, 'No se encontró una compañía seleccionada');
}
// Cargar pedimento_codes (datos de referencia)
const pedimentoCodesPromise = authenticatedFetch(
'v1/public/refrence_data/pedimento-codes?page=1&page_size=100',
{},
cookies,
fetch
);
// Cargar customs_sections (datos de referencia)
const customsSectionsPromise = authenticatedFetch(
'v1/public/refrence_data/customs-sections?page=1&page_size=100',
{},
cookies,
fetch
);
// Cargar customs_brokers (datos de referencia)
const customsBrokersPromise = authenticatedFetch(
`v1/a76/customs-brokers?company_id=${companyId}`,
{},
cookies,
fetch
);
// Cargar clientes (para el select de client_id)
const clientsPromise = authenticatedFetch(
`v1/a76/clients-providers?company_id=${companyId}&type=client&page=1&page_size=1000`,
{},
cookies,
fetch
);
// Cargar code-pedimento-regimens (para interdependencia de campos)
const codePedimentoRegimensPromise = authenticatedFetch(
'v1/public/refrence_data/code-pedimento-regimens?page=1&page_size=100',
{},
cookies,
fetch
);
// Si el ID es "new", es una creación
if (params.id === 'new') {
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
pedimentoCodesPromise,
customsSectionsPromise,
customsBrokersPromise,
clientsPromise,
codePedimentoRegimensPromise
]);
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
return {
pedimento: null,
pedimentoId: null,
isCreate: true
isCreate: true,
pedimentoCodes: pedimentoCodes.items || [],
customsSections: customsSections.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || []
};
}
@@ -24,12 +90,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
}
try {
// Obtener el company_id de la cookie
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
throw error(400, 'No se encontró una compañía seleccionada');
}
// Cargar el pedimento desde el backend usando authenticatedFetch
const response = await authenticatedFetch(
@@ -51,10 +112,30 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
const pedimento = await response.json();
// Cargar pedimento_codes y customs_sections
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
pedimentoCodesPromise,
customsSectionsPromise,
customsBrokersPromise,
clientsPromise,
codePedimentoRegimensPromise
]);
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
return {
pedimento,
pedimentoId,
isCreate: false
isCreate: false,
pedimentoCodes: pedimentoCodes.items || [],
customsSections: customsSections.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || []
};
} catch (e) {
console.error('Error loading pedimento:', e);

View File

@@ -7,6 +7,18 @@
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import {
ArrowLeft,
CircleAlert,
CircleCheck,
FileText,
Calendar,
CreditCard,
Truck,
ShieldCheck,
LoaderCircle,
Save
} from 'lucide-svelte';
import type { PageData } from './$types';
// Importar los componentes de cada pestaña (ahora sin botones de guardar propios)
@@ -18,8 +30,27 @@
// Importar solo la API de pedimentos
import { pedimentosApi, type CreatePedimentoData, type UpdatePedimentoData } from '$lib/api/dashboard/a76/pedimentos';
import type { PedimentoCode } from '$lib/api/dashboard/refrence_data/pedimento_codes';
import type { CustomsSection } from '$lib/api/dashboard/refrence_data/customs_sections';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import type { CodePedimentoRegimen } from '$lib/api/dashboard/refrence_data/code_pedimento_regimens';
let { data }: { data: PageData } = $props();
interface ExtendedPageData {
pedimentoId?: number | null;
pedimento?: any;
isCreate?: boolean;
pedimentoCodes?: PedimentoCode[];
customsSections?: CustomsSection[];
customsBrokers?: CustomsBroker[];
clients?: ClientProvider[];
codePedimentoRegimens?: CodePedimentoRegimen[];
user?: any;
companies?: any[];
authenticated?: boolean;
}
let { data }: { data: ExtendedPageData } = $props();
let activeTab = $state('general');
let saving = $state(false);
@@ -27,7 +58,7 @@
let success = $state(false);
// ID del pedimento
let pedimentoId = $state<number | null>(data.pedimentoId);
let pedimentoId = $state<number | null>(data.pedimentoId ?? null);
// Referencias a los componentes de formulario para obtener sus datos
let generalFormData = $state<any>(null);
@@ -66,6 +97,7 @@
success = false;
try {
// Validar campos requeridos para creación
if (data.isCreate && generalFormData) {
const requiredFields = {
@@ -109,28 +141,95 @@
usd_value: generalFormData?.usd_value || undefined,
paid_price: generalFormData?.paid_price || undefined,
gross_weight: generalFormData?.gross_weight || undefined,
exchange_rate: generalFormData?.exchange_rate || undefined,
// Sub-recursos
pedimento_dates: (datesFormData?.entry_date || datesFormData?.pedimento_date || datesFormData?.payment_date) ? {
entry_date: datesFormData.entry_date || null,
pedimento_date: datesFormData.pedimento_date || null,
payment_date: datesFormData.payment_date || null
} : undefined,
pedimento_payments: (paymentsFormData?.payment_form || paymentsFormData?.bank_identifier) ? {
payment_form: paymentsFormData.payment_form || null,
bank_identifier: paymentsFormData.bank_identifier || null
} : undefined,
pedimento_transport_means: (transportFormData?.arrival_key || transportFormData?.arrival_data || transportFormData?.departure_key || transportFormData?.departure_data) ? {
arrival_key: transportFormData.arrival_key || null,
arrival_data: transportFormData.arrival_data || null,
departure_key: transportFormData.departure_key || null,
departure_data: transportFormData.departure_data || null
} : undefined,
pedimento_validation: validationFormData?.document ? {
document: validationFormData.document || null
} : undefined
exchange_rate: generalFormData?.exchange_rate || undefined
};
// Solo agregar sub-recursos en modo UPDATE (no en CREATE)
// Y solo si tienen valores reales (no enviar objetos vacíos/null)
if (!data.isCreate) {
// Dates - solo enviar si hay al menos un campo con valor
if (datesFormData) {
const hasDateValue = datesFormData.entry_date || datesFormData.pedimento_date ||
datesFormData.payment_date || datesFormData.rectification_payment_date ||
datesFormData.extraction_date || datesFormData.submission_date ||
datesFormData.eucan_date || datesFormData.original_date ||
datesFormData.start_date || datesFormData.end_date;
if (hasDateValue) {
payload.pedimento_dates = {
entry_date: datesFormData.entry_date || null,
pedimento_date: datesFormData.pedimento_date || null,
payment_date: datesFormData.payment_date || null,
rectification_payment_date: datesFormData.rectification_payment_date || null,
extraction_date: datesFormData.extraction_date || null,
submission_date: datesFormData.submission_date || null,
eucan_date: datesFormData.eucan_date || null,
original_date: datesFormData.original_date || null,
start_date: datesFormData.start_date || null,
end_date: datesFormData.end_date || null,
};
}
}
// Payments - solo enviar si hay al menos un campo con valor
if (paymentsFormData) {
const hasPaymentValue = paymentsFormData.acknowledgment || paymentsFormData.operation_number ||
paymentsFormData.bank_code || paymentsFormData.cashier || paymentsFormData.date ||
paymentsFormData.time || paymentsFormData.shift || paymentsFormData.total_cash_paid ||
paymentsFormData.total_contributions || paymentsFormData.counter_payment ||
paymentsFormData.pece_code;
if (hasPaymentValue) {
payload.pedimento_payments = {
acknowledgment: paymentsFormData.acknowledgment || null,
operation_number: paymentsFormData.operation_number || null,
bank_code: paymentsFormData.bank_code || null,
cashier: paymentsFormData.cashier || null,
date: paymentsFormData.date || null,
time: paymentsFormData.time || null,
shift: paymentsFormData.shift || null,
total_cash_paid: paymentsFormData.total_cash_paid || null,
total_contributions: paymentsFormData.total_contributions || null,
counter_payment: paymentsFormData.counter_payment || null,
pece_code: paymentsFormData.pece_code || null,
};
}
}
// Transport - solo enviar si hay al menos un campo con valor
if (transportFormData) {
const hasTransportValue = transportFormData.destination || transportFormData.entry_exit ||
transportFormData.arrival || transportFormData.departure;
if (hasTransportValue) {
payload.pedimento_transport_means = {
destination: transportFormData.destination || null,
entry_exit: transportFormData.entry_exit || null,
arrival: transportFormData.arrival || null,
departure: transportFormData.departure || null
};
}
}
// Validation - solo enviar si hay al menos un campo con valor
if (validationFormData) {
const hasValidationValue = validationFormData.validator || validationFormData.validation_ack ||
validationFormData.pre_ack || validationFormData.line_signature ||
validationFormData.electronic_signature || validationFormData.certificate_number ||
validationFormData.validator_id || validationFormData.responsible_id;
if (hasValidationValue) {
payload.pedimento_validation = {
validator: validationFormData.validator || null,
validation_ack: validationFormData.validation_ack || null,
pre_ack: validationFormData.pre_ack || null,
line_signature: validationFormData.line_signature || null,
electronic_signature: validationFormData.electronic_signature || null,
certificate_number: validationFormData.certificate_number || null,
validator_id: validationFormData.validator_id || null,
responsible_id: validationFormData.responsible_id || null
};
}
}
}
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
if (payload[key as keyof typeof payload] === undefined) {
@@ -140,9 +239,9 @@
let newPedimentoId = pedimentoId;
if (data.isCreate) {
if (data.isCreate) {
// Crear nuevo pedimento con todos sus sub-recursos
const response = await pedimentosApi.create(payload as CreatePedimentoData);
const response = await pedimentosApi.create(payload as CreatePedimentoData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear el pedimento';
throw new Error(errorMsg);
@@ -153,12 +252,12 @@
// Redirigir a la página de edición
await goto(`/dashboard/pedimentos/edit/${newPedimentoId}`);
return;
} else {
} else {
// Actualizar pedimento existente con todos sus sub-recursos
const response = await pedimentosApi.update(pedimentoId!, payload as UpdatePedimentoData);
const response = await pedimentosApi.update(pedimentoId!, payload as UpdatePedimentoData);
if (response.error) throw new Error(response.error);
}
success = true;
setTimeout(() => {
success = false;
@@ -185,20 +284,7 @@
<div class="space-y-1">
<div class="flex items-center gap-3">
<Button variant="ghost" size="icon" onclick={handleBack}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>
<ArrowLeft size={20} />
</Button>
<h1 class="text-3xl font-bold tracking-tight">
{#if data.isCreate}
@@ -230,21 +316,7 @@
<!-- Alertas globales -->
{#if error}
<Alert.Root variant="destructive">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>
<CircleAlert size={16} />
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
@@ -252,216 +324,105 @@
{#if success}
<Alert.Root>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
<path d="m9 12 2 2 4-4" />
</svg>
<CircleCheck size={16} />
<Alert.Title>Éxito</Alert.Title>
<Alert.Description>Todos los cambios se guardaron correctamente</Alert.Description>
</Alert.Root>
{/if}
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab} class="space-y-4">
<Tabs.List class="grid w-full grid-cols-5">
<Tabs.Trigger value="general" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
</svg>
General
</Tabs.Trigger>
<Tabs.Trigger value="dates" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
<line x1="16" x2="16" y1="2" y2="6" />
<line x1="8" x2="8" y1="2" y2="6" />
<line x1="3" x2="21" y1="10" y2="10" />
</svg>
Fechas
</Tabs.Trigger>
<Tabs.Trigger value="payments" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<line x1="2" x2="22" y1="10" y2="10" />
</svg>
Pagos
</Tabs.Trigger>
<Tabs.Trigger value="transport" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2" />
<path d="M15 18H9" />
<path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14" />
<circle cx="17" cy="18" r="2" />
<circle cx="7" cy="18" r="2" />
</svg>
Transporte
</Tabs.Trigger>
<Tabs.Trigger value="validation" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="m9 12 2 2 4-4" />
</svg>
Validación
</Tabs.Trigger>
</Tabs.List>
<!-- Contenido de las tabs con padding inferior para el footer flotante -->
<div class="pb-48">
<Tabs.Root bind:value={activeTab} class="space-y-4">
<Tabs.Content value="general">
<GeneralTabForm
pedimento={data.pedimento}
bind:formData={generalFormData}
pedimentoCodes={data.pedimentoCodes || []}
customsSections={data.customsSections || []}
customsBrokers={data.customsBrokers || []}
clients={data.clients || []}
codePedimentoRegimens={data.codePedimentoRegimens || []}
/>
</Tabs.Content>
<Tabs.Content value="general">
<GeneralTabForm
pedimento={data.pedimento}
bind:formData={generalFormData}
/>
</Tabs.Content>
<Tabs.Content value="dates">
<DatesTabForm
pedimento={data.pedimento}
bind:formData={datesFormData}
bind:exists={datesExists}
/>
</Tabs.Content>
<Tabs.Content value="dates">
<DatesTabForm
pedimentoId={pedimentoId}
bind:formData={datesFormData}
bind:exists={datesExists}
/>
</Tabs.Content>
<Tabs.Content value="payments">
<PaymentsTabForm
pedimento={data.pedimento}
bind:formData={paymentsFormData}
bind:exists={paymentsExists}
/>
</Tabs.Content>
<Tabs.Content value="payments">
<PaymentsTabForm
pedimentoId={pedimentoId}
bind:formData={paymentsFormData}
bind:exists={paymentsExists}
/>
</Tabs.Content>
<Tabs.Content value="transport">
<TransportTabForm
pedimento={data.pedimento}
bind:formData={transportFormData}
bind:exists={transportExists}
/>
</Tabs.Content>
<Tabs.Content value="transport">
<TransportTabForm
pedimentoId={pedimentoId}
bind:formData={transportFormData}
bind:exists={transportExists}
/>
</Tabs.Content>
<Tabs.Content value="validation">
<ValidationTabForm
pedimentoId={pedimentoId}
bind:formData={validationFormData}
bind:exists={validationExists}
/>
</Tabs.Content>
</Tabs.Root>
<!-- Botón de guardar global -->
<Card.Root>
<Card.Content class="pt-6">
<div class="flex justify-end gap-3">
<Button type="button" variant="outline" onclick={handleBack} disabled={saving}>
Cancelar
</Button>
<Button onclick={handleSaveAll} disabled={saving}>
{#if saving}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Guardando todos los cambios...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
Guardar Todos los Cambios
{/if}
</Button>
</div>
</Card.Content>
</Card.Root>
<Tabs.Content value="validation">
<ValidationTabForm
pedimento={data.pedimento}
bind:formData={validationFormData}
bind:exists={validationExists}
/>
</Tabs.Content>
</Tabs.Root>
</div>
</div>
<!-- Footer fijo en la parte inferior - Fuera del contenedor principal -->
<div class="fixed bottom-0 inset-x-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-50 group-has-[[data-sidebar]]/sidebar-wrapper:left-[var(--sidebar-width)]">
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab}>
<Tabs.List class="grid w-full grid-cols-5">
<Tabs.Trigger value="general" disabled={false}>
<FileText size={16} class="mr-2" />
General
</Tabs.Trigger>
<Tabs.Trigger value="dates" disabled={false}>
<Calendar size={16} class="mr-2" />
Fechas
</Tabs.Trigger>
<Tabs.Trigger value="payments" disabled={false}>
<CreditCard size={16} class="mr-2" />
Pagos
</Tabs.Trigger>
<Tabs.Trigger value="transport" disabled={false}>
<Truck size={16} class="mr-2" />
Transporte
</Tabs.Trigger>
<Tabs.Trigger value="validation" disabled={false}>
<ShieldCheck size={16} class="mr-2" />
Validación
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<!-- Botones de acción -->
<div class="flex justify-end gap-3">
<Button type="button" variant="outline" onclick={handleBack} disabled={saving}>
Cancelar
</Button>
<Button onclick={handleSaveAll} disabled={saving}>
{#if saving}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
Guardando todos los cambios...
{:else}
<Save size={16} class="mr-2" />
Guardar Todos los Cambios
{/if}
</Button>
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Registro
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Contenedor
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo País
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,22 +121,8 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
Nueva Moneda
<Plus class="mr-2" size={16} />
Nuevo Tipo de Moneda
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nueva Sección
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Recinto
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/incoterms/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Incoterm
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/invoice_types/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Factura
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Material
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Método de Pago
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/pedimento_codes/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,22 +121,8 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
Nueva Clave de Pedimento
<Plus class="mr-2" size={16} />
Nuevo Código de Pedimento
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Régimen de Pedimento
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Sector
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Estado
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -8,6 +8,7 @@
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { Plus, RefreshCw } from 'lucide-svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Modo de Transporte
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/transport_types/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus class="mr-2" size={16} />
Nuevo Tipo de Transporte
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>

View File

@@ -6,6 +6,7 @@
import CreateEditDialog from '$lib/components/dashboard/reference_data/valuation_methods/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
import { browser } from '$app/environment';
@@ -120,21 +121,7 @@
</p>
</div>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
<Plus size={16} class="mr-2" />
Nuevo Método de Valoración
</Button>
</div>
@@ -160,21 +147,7 @@
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
<RefreshCw size={16} class="mr-2" />
Actualizar
</Button>
</div>

View File

@@ -22,7 +22,7 @@
# se actualiza con el ID real del tenant creado en PostgreSQL.
###############################################################################
set -e # Salir si hay algún error
# set -e # Comentado para permitir que el script continúe aunque algunos comandos fallen (ej: mapper ya existe)
# Colores para output
RED='\033[0;31m'
@@ -309,87 +309,84 @@ echo -e "\n${YELLOW}[5/8] Configurando mappers para tenant_id...${NC}"
if [ -n "$BACKEND_CLIENT_ID" ]; then
echo "Configurando mapper para Backend..."
# Obtener el dedicated scope del cliente backend
BACKEND_SCOPES=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${BACKEND_CLIENT_ID}/optional-client-scopes" \
# Verificar si el mapper tenant_id ya existe en el cliente
MAPPER_TENANT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${BACKEND_CLIENT_ID}/protocol-mappers/models" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json")
-H "Content-Type: application/json" | grep -o "\"name\":\"tenant-id-mapper\"")
# Buscar el scope dedicado
BACKEND_DEDICATED_SCOPE_ID=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" | grep -o "\"id\":\"[^\"]*\",\"name\":\"anexo76-backend-dedicated\"" | grep -o "\"id\":\"[^\"]*" | sed 's/"id":"//')
if [ -n "$BACKEND_DEDICATED_SCOPE_ID" ]; then
# Verificar si el mapper tenant_id ya existe
MAPPER_TENANT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${BACKEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \
if [ -z "$MAPPER_TENANT_EXISTS" ]; then
# Crear mapper para tenant_id directamente en el cliente
CREATE_MAPPER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${BACKEND_CLIENT_ID}/protocol-mappers/models" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" | grep -o "\"name\":\"tenant-id-mapper\"")
-H "Content-Type: application/json" \
-d '{
"name": "tenant-id-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}')
if [ -z "$MAPPER_TENANT_EXISTS" ]; then
# Crear mapper para tenant_id
curl -s -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${BACKEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "tenant-id-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}'
HTTP_CODE=$(echo "$CREATE_MAPPER_RESPONSE" | tail -n1)
if [ "$HTTP_CODE" = "201" ]; then
echo -e "${GREEN}✓ Mapper tenant_id creado para Backend${NC}"
else
echo -e "${YELLOW}Mapper tenant_id ya existe para Backend${NC}"
echo -e "${YELLOW}Error al crear mapper para Backend (HTTP ${HTTP_CODE})${NC}"
echo "Respuesta: $(echo "$CREATE_MAPPER_RESPONSE" | head -n -1)"
fi
else
echo -e "${YELLOW}⚠ Mapper tenant_id ya existe para Backend${NC}"
fi
fi
# 4.2 Configurar mapper para Frontend
if [ -n "$FRONTEND_CLIENT_ID" ]; then
echo "Configurando mapper para Frontend..."
# Buscar el scope dedicado del frontend
FRONTEND_DEDICATED_SCOPE_ID=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes" \
# Verificar si el mapper tenant_id ya existe en el cliente
MAPPER_TENANT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${FRONTEND_CLIENT_ID}/protocol-mappers/models" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" | grep -o "\"id\":\"[^\"]*\",\"name\":\"anexo76-frontend-dedicated\"" | grep -o "\"id\":\"[^\"]*" | sed 's/"id":"//')
-H "Content-Type: application/json" | grep -o "\"name\":\"tenant-id-mapper\"")
if [ -n "$FRONTEND_DEDICATED_SCOPE_ID" ]; then
# Verificar si el mapper tenant_id ya existe
MAPPER_TENANT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${FRONTEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \
if [ -z "$MAPPER_TENANT_EXISTS" ]; then
# Crear mapper para tenant_id directamente en el cliente
CREATE_MAPPER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${FRONTEND_CLIENT_ID}/protocol-mappers/models" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" | grep -o "\"name\":\"tenant-id-mapper\"")
-H "Content-Type: application/json" \
-d '{
"name": "tenant-id-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}')
if [ -z "$MAPPER_TENANT_EXISTS" ]; then
# Crear mapper para tenant_id
curl -s -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${FRONTEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "tenant-id-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}'
HTTP_CODE=$(echo "$CREATE_MAPPER_RESPONSE" | tail -n1)
if [ "$HTTP_CODE" = "201" ]; then
echo -e "${GREEN}✓ Mapper tenant_id creado para Frontend${NC}"
else
echo -e "${YELLOW}Mapper tenant_id ya existe para Frontend${NC}"
echo -e "${YELLOW}Error al crear mapper para Frontend (HTTP ${HTTP_CODE})${NC}"
echo "Respuesta: $(echo "$CREATE_MAPPER_RESPONSE" | head -n -1)"
fi
else
echo -e "${YELLOW}⚠ Mapper tenant_id ya existe para Frontend${NC}"
fi
fi
###############################################################################
# 6. Crear usuario demo en Keycloak
###############################################################################