Desarrollo del sistema de crud, ademas de homogenizar el estilo de las tablas
This commit is contained in:
@@ -340,7 +340,7 @@ class DodaUpdateDTO(BaseModel):
|
||||
class DodaResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de un DODA"""
|
||||
|
||||
sys_id: int
|
||||
id: int
|
||||
integration_number: Optional[str] = None
|
||||
doda_date: Optional[int] = None
|
||||
doda_time: Optional[int] = None
|
||||
@@ -383,7 +383,7 @@ class DodaResponseDTO(BaseModel):
|
||||
class DodaDetailResponseDTO(BaseModel):
|
||||
"""DTO detallado para responder con todos los datos de un DODA"""
|
||||
|
||||
sys_id: int
|
||||
id: int
|
||||
integration_number: Optional[str] = None
|
||||
doda_date: Optional[int] = None
|
||||
doda_time: Optional[int] = None
|
||||
|
||||
@@ -68,7 +68,7 @@ class ElectronicNoticeUpdateDTO(BaseModel):
|
||||
class ElectronicNoticeResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de un aviso electrónico"""
|
||||
|
||||
sys_id: int
|
||||
id: int
|
||||
notice_number: Optional[str] = None
|
||||
year: Optional[str] = None
|
||||
patent: Optional[str] = None
|
||||
|
||||
@@ -30,22 +30,27 @@ class EquivalencyItemResponse(EquivalencyItemBase):
|
||||
|
||||
|
||||
class EquivalencyBase(BaseModel):
|
||||
identifier: str = Field(..., max_length=10, description="Identifier")
|
||||
fraccion_mex: str = Field(..., max_length=10, description="Fraccion MX (Identifier)")
|
||||
fraccion_us: str = Field(..., max_length=100, description="Fraccion US (External Field)")
|
||||
description: Optional[str] = Field(
|
||||
None, max_length=200, description="Description")
|
||||
|
||||
|
||||
class EquivalencyCreate(EquivalencyBase):
|
||||
items: Optional[List[EquivalencyItemCreate]] = []
|
||||
pass
|
||||
|
||||
|
||||
class EquivalencyUpdate(BaseModel):
|
||||
identifier: Optional[str] = Field(None, max_length=10)
|
||||
fraccion_mex: Optional[str] = Field(None, max_length=10)
|
||||
fraccion_us: Optional[str] = Field(None, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
|
||||
class EquivalencyResponse(EquivalencyBase):
|
||||
id: int
|
||||
items: List[EquivalencyItemResponse] = []
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -16,7 +16,9 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
identifier: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String(200), nullable=True)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import Equivalency, EquivalencyItem
|
||||
@@ -20,14 +21,26 @@ class EquivalencyService:
|
||||
query = db.query(Equivalency).filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
)
|
||||
).options(joinedload(Equivalency.items))
|
||||
|
||||
if filters:
|
||||
# Add filters here if needed
|
||||
pass
|
||||
if 'fraccion_mex' in filters:
|
||||
query = query.filter(Equivalency.identifier.ilike(f"%{filters['fraccion_mex']}%"))
|
||||
if 'description' in filters:
|
||||
query = query.filter(Equivalency.description.ilike(f"%{filters['description']}%"))
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Map internal fields to DTO fields
|
||||
for item in items:
|
||||
item.fraccion_mex = item.identifier
|
||||
# Try to find the first item to get fraccion_us
|
||||
if item.items:
|
||||
item.fraccion_us = item.items[0].external_field
|
||||
else:
|
||||
item.fraccion_us = ""
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -37,11 +50,20 @@ class EquivalencyService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
return db.query(Equivalency).filter(
|
||||
item = db.query(Equivalency).filter(
|
||||
Equivalency.id == id,
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
).first()
|
||||
).options(joinedload(Equivalency.items)).first()
|
||||
|
||||
if item:
|
||||
item.fraccion_mex = item.identifier
|
||||
if item.items:
|
||||
item.fraccion_us = item.items[0].external_field
|
||||
else:
|
||||
item.fraccion_us = ""
|
||||
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
@@ -50,50 +72,122 @@ class EquivalencyService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Equivalency:
|
||||
db_obj = Equivalency(
|
||||
identifier=data.identifier,
|
||||
description=data.description,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
try:
|
||||
# Create Parent
|
||||
db_obj = Equivalency(
|
||||
identifier=data.fraccion_mex,
|
||||
description=data.description,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.flush() # Flush to get ID
|
||||
|
||||
if data.items:
|
||||
for item_data in data.items:
|
||||
item = EquivalencyItem(
|
||||
**item_data.model_dump(),
|
||||
equivalency_id=db_obj.id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
# Create Child Item (mapping fraccion_mex -> original_field, fraccion_us -> external_field)
|
||||
item = EquivalencyItem(
|
||||
equivalency_id=db_obj.id,
|
||||
original_field=data.fraccion_mex, # Must exist in units_of_measure
|
||||
external_field=data.fraccion_us,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
return db_obj
|
||||
|
||||
# Map for response
|
||||
db_obj.fraccion_mex = db_obj.identifier
|
||||
db_obj.fraccion_us = item.external_field
|
||||
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
|
||||
print(f"IntegrityError in create: {error_msg}")
|
||||
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"La Fracción MX '{data.fraccion_mex}' no es válida. Debe existir en el catálogo de Unidades de Medida."
|
||||
)
|
||||
if "uq_equivalency_identifier" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ya existe una equivalencia para la Fracción MX '{data.fraccion_mex}'."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error in create: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: EquivalencyUpdate,
|
||||
tenant_id: int,
|
||||
data: EquivalencyUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
if key != 'items': # Handle items separately if needed, or ignore for now as per original code
|
||||
setattr(db_obj, key, value)
|
||||
try:
|
||||
if data.fraccion_mex:
|
||||
db_obj.identifier = data.fraccion_mex
|
||||
if data.description:
|
||||
db_obj.description = data.description
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
# Update Item
|
||||
item = None
|
||||
if db_obj.items:
|
||||
item = db_obj.items[0]
|
||||
|
||||
if item:
|
||||
if data.fraccion_us:
|
||||
item.external_field = data.fraccion_us
|
||||
if data.fraccion_mex:
|
||||
item.original_field = data.fraccion_mex
|
||||
else:
|
||||
# Create if missing
|
||||
if data.fraccion_us or data.fraccion_mex:
|
||||
item = EquivalencyItem(
|
||||
equivalency_id=db_obj.id,
|
||||
original_field=data.fraccion_mex or db_obj.identifier,
|
||||
external_field=data.fraccion_us or "",
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Map for response
|
||||
db_obj.fraccion_mex = db_obj.identifier
|
||||
if db_obj.items:
|
||||
db_obj.fraccion_us = db_obj.items[0].external_field
|
||||
else:
|
||||
db_obj.fraccion_us = ""
|
||||
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
|
||||
print(f"IntegrityError in update: {error_msg}")
|
||||
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"La Fracción MX '{data.fraccion_mex or db_obj.identifier}' no es válida. Debe existir en el catálogo de Unidades de Medida."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error in update: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
@@ -110,7 +204,6 @@ class EquivalencyService:
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
class EquivalencyItemService:
|
||||
@staticmethod
|
||||
def get_all(
|
||||
@@ -125,11 +218,6 @@ class EquivalencyItemService:
|
||||
EquivalencyItem.tenant_id == tenant_id,
|
||||
EquivalencyItem.company_id == company_id
|
||||
)
|
||||
|
||||
if filters and "equivalency_id" in filters:
|
||||
query = query.filter(
|
||||
EquivalencyItem.equivalency_id == filters["equivalency_id"])
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
@@ -154,13 +242,6 @@ class EquivalencyItemService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
# Note: equivalency_id should be in data or handled by the caller if it's a nested route
|
||||
# But TenantCRUDRoutes for child resources might pass it in filters or we need to handle it.
|
||||
# For now, assuming it's in data or we don't use child resource feature yet.
|
||||
|
||||
# If using child resource, the parent_id is usually passed in the path.
|
||||
# But TenantCRUDRoutes passes the body.
|
||||
|
||||
db_obj = EquivalencyItem(
|
||||
**data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
@@ -180,8 +261,9 @@ class EquivalencyItemService:
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
db_obj = EquivalencyItem(
|
||||
**data.model_dump(),
|
||||
equivalency_id=equivalency_id,
|
||||
original_field=data.original_field,
|
||||
external_field=data.external_field,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
@@ -194,19 +276,17 @@ class EquivalencyItemService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: EquivalencyItemUpdate,
|
||||
tenant_id: int,
|
||||
data: EquivalencyItemUpdate,
|
||||
company_id: int
|
||||
) -> Optional[EquivalencyItem]:
|
||||
db_obj = EquivalencyItemService.get_by_id(
|
||||
db, id, tenant_id, company_id)
|
||||
db_obj = EquivalencyItemService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
@@ -218,11 +298,9 @@ class EquivalencyItemService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> bool:
|
||||
db_obj = EquivalencyItemService.get_by_id(
|
||||
db, id, tenant_id, company_id)
|
||||
db_obj = EquivalencyItemService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@@ -68,8 +68,8 @@ class IdentifierService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: IdentifierUpdate,
|
||||
tenant_id: int,
|
||||
data: IdentifierUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Identifier]:
|
||||
db_obj = IdentifierService.get_by_id(db, id, tenant_id, company_id)
|
||||
|
||||
@@ -63,8 +63,8 @@ class INPCService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: INPCUpdate,
|
||||
tenant_id: int,
|
||||
data: INPCUpdate,
|
||||
company_id: int
|
||||
) -> Optional[INPC]:
|
||||
db_obj = INPCService.get_by_id(db, id, tenant_id, company_id)
|
||||
|
||||
@@ -56,32 +56,3 @@ class PrevalidatorResponseDTO(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrevalidatorUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar un prevalidador"""
|
||||
|
||||
customs_prevalidator: Optional[str] = Field(
|
||||
None, max_length=20, description="Customs prevalidator"
|
||||
)
|
||||
patent_prevalidator: Optional[str] = Field(
|
||||
None, max_length=20, description="Patent prevalidator"
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
None, max_length=50, description="Description"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrevalidatorResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de un prevalidador"""
|
||||
|
||||
code: str
|
||||
customs_prevalidator: Optional[str] = None
|
||||
patent_prevalidator: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -120,8 +120,8 @@ class PrevalidatorService:
|
||||
def update(
|
||||
db: Session,
|
||||
prevalidator_id: int,
|
||||
prevalidator_data: PrevalidatorUpdateDTO,
|
||||
tenant_id: int,
|
||||
prevalidator_data: PrevalidatorUpdateDTO,
|
||||
company_id: int
|
||||
) -> Optional[Prevalidator]:
|
||||
"""Update a prevalidator"""
|
||||
|
||||
@@ -40,7 +40,7 @@ class SignatureResponseDTO(BaseModel):
|
||||
code: str
|
||||
signature: Optional[str] = None
|
||||
photo_path: Optional[str] = None
|
||||
intenant_id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
class Config:
|
||||
|
||||
@@ -5,6 +5,8 @@ Capa de servicio para lógica de negocio de firmas
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import dto, models
|
||||
@@ -81,10 +83,18 @@ class SignatureService:
|
||||
new_signature = models.Signature(
|
||||
**signature_data.model_dump(), tenant_id=tenant_id, company_id=company_id
|
||||
)
|
||||
db.add(new_signature)
|
||||
db.commit()
|
||||
db.refresh(new_signature)
|
||||
return new_signature
|
||||
try:
|
||||
db.add(new_signature)
|
||||
db.commit()
|
||||
db.refresh(new_signature)
|
||||
return new_signature
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
# Constraint names: signatures_code_unique (code, tenant_id, company_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ya existe una firma con ese código para esta compañía",
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import UnitConversion
|
||||
from .dto import UnitConversionCreate, UnitConversionUpdate
|
||||
@@ -51,10 +54,18 @@ class UnitConversionService:
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
try:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
# Puede ser FK de unidades o duplicado de (from_unit_code, to_unit_code, tenant, company)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Verifica que los códigos de unidad existan y que la conversión no esté duplicada",
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
@@ -72,9 +83,16 @@ class UnitConversionService:
|
||||
for key, value in update_dict.items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Verifica que los códigos de unidad existan y que la conversión no esté duplicada",
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
|
||||
30
backend/check_db.py
Normal file
30
backend/check_db.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy import create_engine, text, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Add the backend directory to the python path
|
||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||
|
||||
from core.config import settings
|
||||
|
||||
def check_equivalencies_columns():
|
||||
engine = create_engine(str(settings.core_database_url))
|
||||
inspector = inspect(engine)
|
||||
|
||||
try:
|
||||
print("Checking equivalencies table columns...")
|
||||
columns = inspector.get_columns('equivalencies', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
print("\nChecking equivalency_items table columns...")
|
||||
columns = inspector.get_columns('equivalency_items', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_equivalencies_columns()
|
||||
30
check_db.py
Normal file
30
check_db.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy import create_engine, text, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Add the backend directory to the python path
|
||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||
|
||||
from core.config import settings
|
||||
|
||||
def check_equivalencies_columns():
|
||||
engine = create_engine(str(settings.core_database_url))
|
||||
inspector = inspect(engine)
|
||||
|
||||
try:
|
||||
print("Checking equivalencies table columns...")
|
||||
columns = inspector.get_columns('equivalencies', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
print("\nChecking equivalency_items table columns...")
|
||||
columns = inspector.get_columns('equivalency_items', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_equivalencies_columns()
|
||||
23
check_uom.py
Normal file
23
check_uom.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add backend to path
|
||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||
|
||||
from core.database import CoreSessionLocal
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
|
||||
def check_units():
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
units = db.query(UnitOfMeasure).limit(10).all()
|
||||
print(f"Found {len(units)} units:")
|
||||
for u in units:
|
||||
print(f"Code: {u.code}, Description: {u.description}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_units()
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
|
||||
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8001:8000"
|
||||
depends_on:
|
||||
postgres-a76:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -72,8 +72,7 @@ export interface DodaPedimentoCreate {
|
||||
|
||||
|
||||
export interface Doda {
|
||||
|
||||
sys_id: number;
|
||||
id: number;
|
||||
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
@@ -107,9 +106,31 @@ export interface DodaCreate {
|
||||
dispatch_customs?: string;
|
||||
customs_sections?: string;
|
||||
patent?: string;
|
||||
pedimentos?: string;
|
||||
caat?: string;
|
||||
transport_identification?: string;
|
||||
fast_id?: string;
|
||||
operation_type?: string;
|
||||
selected?: boolean;
|
||||
user_selected?: string;
|
||||
last_user?: string;
|
||||
responsible?: string;
|
||||
carrier?: string;
|
||||
shipments?: string;
|
||||
pedimento_type?: string;
|
||||
original_chain?: string;
|
||||
serial_number?: string;
|
||||
electronic_signature?: string;
|
||||
transaction_number?: string;
|
||||
status?: string;
|
||||
linq_sat_qr?: string;
|
||||
sat_certificate?: string;
|
||||
sat_digital_seal?: string;
|
||||
xml_doda_sent_path?: string;
|
||||
xml_doda_response_path?: string;
|
||||
sat_original_chain?: string;
|
||||
customs_clearance?: number;
|
||||
unique_badge_number?: string;
|
||||
}
|
||||
|
||||
export interface DodaUpdate extends Partial<DodaCreate> {}
|
||||
@@ -126,32 +147,40 @@ export interface DodaListResponse {
|
||||
export async function getDodas(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<DodaListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/doda?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getDoda(id: number): Promise<Doda> {
|
||||
const response = await api.get(`/v1/a76/doda/${id}`);
|
||||
export async function getDoda(id: number, companyId?: number): Promise<Doda> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/doda/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createDoda(data: DodaCreate): Promise<Doda> {
|
||||
const response = await api.post('/v1/a76/doda', data);
|
||||
export async function createDoda(data: DodaCreate, companyId: number): Promise<Doda> {
|
||||
const response = await api.post(`/v1/a76/doda?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateDoda(id: number, data: DodaUpdate): Promise<Doda> {
|
||||
const response = await api.patch(`/v1/a76/doda/${id}`, data);
|
||||
export async function updateDoda(id: number, data: DodaUpdate, companyId: number): Promise<Doda> {
|
||||
const response = await api.patch(`/v1/a76/doda/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteDoda(id: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/doda/${id}`);
|
||||
export async function deleteDoda(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/doda/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -17,8 +17,9 @@ export interface ElectronicNotice {
|
||||
certificate_number?: string;
|
||||
|
||||
// Mixins
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
@@ -49,34 +50,41 @@ export interface ElectronicNoticeListResponse {
|
||||
export async function getElectronicNotices(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<ElectronicNoticeListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
// Agregamos '/' al final
|
||||
const response = await api.get(`/a76/electronic_notices/?${params.toString()}`);
|
||||
const response = await api.get(`/v1/a76/electronic-notices/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getElectronicNotice(id: number): Promise<ElectronicNotice> {
|
||||
const response = await api.get(`/a76/electronic_notices/${id}`);
|
||||
export async function getElectronicNotice(id: number, companyId?: number): Promise<ElectronicNotice> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/electronic-notices/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createElectronicNotice(data: ElectronicNoticeCreate): Promise<ElectronicNotice> {
|
||||
const response = await api.post('/a76/electronic_notices', data);
|
||||
export async function createElectronicNotice(data: ElectronicNoticeCreate, companyId: number): Promise<ElectronicNotice> {
|
||||
const response = await api.post(`/v1/a76/electronic-notices/?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate): Promise<ElectronicNotice> {
|
||||
const response = await api.patch(`/a76/electronic_notices/${id}`, data);
|
||||
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate, companyId: number): Promise<ElectronicNotice> {
|
||||
const response = await api.put(`/v1/a76/electronic-notices/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteElectronicNotice(id: number): Promise<void> {
|
||||
await api.delete(`/a76/electronic_notices/${id}`);
|
||||
export async function deleteElectronicNotice(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/electronic-notices/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,62 +2,71 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Equivalency {
|
||||
id: number;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyCreate {
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyUpdate extends Partial<EquivalencyCreate> {}
|
||||
export interface EquivalencyUpdate {
|
||||
fraccion_mex?: string;
|
||||
fraccion_us?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyListResponse {
|
||||
items: Equivalency[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: Equivalency[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getEquivalencies(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<EquivalencyListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/equivalencies?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/equivalencies/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getEquivalency(id: number): Promise<Equivalency> {
|
||||
const response = await api.get(`/a76/equivalencies/${id}`);
|
||||
return response.data;
|
||||
export async function getEquivalency(id: number, companyId: number): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createEquivalency(data: EquivalencyCreate): Promise<Equivalency> {
|
||||
const response = await api.post('/a76/equivalencies', data);
|
||||
return response.data;
|
||||
export async function createEquivalency(
|
||||
data: EquivalencyCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateEquivalency(id: number, data: EquivalencyUpdate): Promise<Equivalency> {
|
||||
const response = await api.patch(`/a76/equivalencies/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateEquivalency(
|
||||
id: number,
|
||||
data: EquivalencyUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteEquivalency(id: number): Promise<void> {
|
||||
await api.delete(`/a76/equivalencies/${id}`);
|
||||
export async function deleteEquivalency(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
// ==========================================
|
||||
// ERROR CLASSIFICATION
|
||||
@@ -64,71 +63,83 @@ export interface ErrorCatalogListResponse {
|
||||
}
|
||||
|
||||
export async function getErrorClassifications(
|
||||
companyId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<ErrorClassificationListResponse>> {
|
||||
): Promise<ErrorClassificationListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/error_classifications?${params.toString()}`);
|
||||
const response = await api.get(`/v1/a76/error-catalogs/classifications/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getErrorClassification(id: number): Promise<ErrorClassification> {
|
||||
const response = await api.get(`/a76/error_classifications/${id}`);
|
||||
export async function getErrorClassification(id: number, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.get(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createErrorClassification(data: ErrorClassificationCreate): Promise<ErrorClassification> {
|
||||
const response = await api.post('/a76/error_classifications', data);
|
||||
export async function createErrorClassification(data: ErrorClassificationCreate, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post(`/v1/a76/error-catalogs/classifications/?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate): Promise<ErrorClassification> {
|
||||
const response = await api.patch(`/a76/error_classifications/${id}`, data);
|
||||
export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteErrorClassification(id: number): Promise<void> {
|
||||
await api.delete(`/a76/error_classifications/${id}`);
|
||||
export async function deleteErrorClassification(id: number, companyId: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
|
||||
}
|
||||
|
||||
// --- Catalogs ---
|
||||
|
||||
export async function getErrorCatalogs(
|
||||
companyId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<ErrorCatalogListResponse>> {
|
||||
): Promise<ErrorCatalogListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/error_catalogs?${params.toString()}`);
|
||||
const response = await api.get(`/v1/a76/error-catalogs/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getErrorCatalog(id: number): Promise<ErrorCatalog> {
|
||||
const response = await api.get(`/a76/error_catalogs/${id}`);
|
||||
export async function getErrorCatalog(id: number, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.get(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createErrorCatalog(data: ErrorCatalogCreate): Promise<ErrorCatalog> {
|
||||
const response = await api.post('/a76/error_catalogs', data);
|
||||
export async function createErrorCatalog(data: ErrorCatalogCreate, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post(`/v1/a76/error-catalogs/?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate): Promise<ErrorCatalog> {
|
||||
const response = await api.patch(`/a76/error_catalogs/${id}`, data);
|
||||
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put(`/v1/a76/error-catalogs/${id}?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteErrorCatalog(id: number): Promise<void> {
|
||||
await api.delete(`/a76/error_catalogs/${id}`);
|
||||
export async function deleteErrorCatalog(id: number, companyId: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export interface INPCCreate {
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
|
||||
}
|
||||
|
||||
export interface INPCUpdate extends Partial<INPCCreate> {}
|
||||
@@ -29,7 +28,6 @@ export interface INPCListResponse {
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export async function getINPCs(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
@@ -43,36 +41,28 @@ export async function getINPCs(
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/v1/a76/inpc/?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/inpc/?${params.toString()}`);
|
||||
}
|
||||
|
||||
|
||||
export async function getINPC(id: number, companyId: number): Promise<INPC> {
|
||||
const response = await api.get(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
return response.data;
|
||||
export async function getINPC(id: number, companyId: number): Promise<ApiResponse<INPC>> {
|
||||
return await api.get(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
|
||||
export async function createINPC(
|
||||
data: INPCCreate,
|
||||
companyId: number
|
||||
): Promise<INPC> {
|
||||
const response = await api.post(`/v1/a76/inpc/?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
): Promise<ApiResponse<INPC>> {
|
||||
return await api.post(`/v1/a76/inpc/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
|
||||
export async function updateINPC(
|
||||
id: number,
|
||||
data: INPCUpdate,
|
||||
companyId: number
|
||||
): Promise<INPC> {
|
||||
|
||||
const response = await api.put(`/v1/a76/inpc/${id}/?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
): Promise<ApiResponse<INPC>> {
|
||||
return await api.put(`/v1/a76/inpc/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteINPC(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
export async function deleteINPC(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,90 @@
|
||||
import type { PaginatedResponse } from '$lib/types';
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Location {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
|
||||
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
location_code: string;
|
||||
location_description: string | null;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
|
||||
export interface LocationCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
location_code: string;
|
||||
location_description?: string | null;
|
||||
}
|
||||
|
||||
|
||||
export interface LocationUpdate {
|
||||
code?: string;
|
||||
description?: string;
|
||||
location_description?: string | null;
|
||||
}
|
||||
|
||||
export interface LocationListResponse {
|
||||
items: Location[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
export interface LocationListResponse extends PaginatedResponse {
|
||||
items: Location[];
|
||||
}
|
||||
|
||||
|
||||
export interface LocationFilters {
|
||||
location_code?: string;
|
||||
location_description?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export async function getLocations(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<LocationListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
companyId: number,
|
||||
filters?: LocationFilters
|
||||
): Promise<LocationListResponse> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
|
||||
const response = await api.get(`/a24/locations?${params.toString()}`);
|
||||
return response.data;
|
||||
if (filters) {
|
||||
if (filters.location_code) params.append('location_code', filters.location_code);
|
||||
if (filters.location_description) params.append('location_description', filters.location_description);
|
||||
if (filters.page) params.append('page', filters.page.toString());
|
||||
if (filters.page_size) params.append('page_size', filters.page_size.toString());
|
||||
}
|
||||
|
||||
return api.get<LocationListResponse>(`/v1/a76/ports/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getLocation(id: number): Promise<Location> {
|
||||
const response = await api.get(`/a24/locations/${id}`);
|
||||
return response.data;
|
||||
export async function getLocation(
|
||||
locationId: number,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.get<Location>(`/v1/a76/ports/${locationId}?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function createLocation(data: LocationCreate): Promise<Location> {
|
||||
const response = await api.post('/a24/locations', data);
|
||||
return response.data;
|
||||
export async function createLocation(
|
||||
data: LocationCreate,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<Location>(`/v1/a76/ports/?${params.toString()}`, {
|
||||
port_code: data.location_code,
|
||||
location_code: data.location_code,
|
||||
description: null,
|
||||
location_description: data.location_description || null,
|
||||
port_type: 'ENTRY'
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateLocation(id: number, data: LocationUpdate): Promise<Location> {
|
||||
const response = await api.patch(`/a24/locations/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateLocation(
|
||||
locationId: number,
|
||||
data: LocationUpdate,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.put<Location>(
|
||||
`/v1/a76/ports/${locationId}?${params.toString()}`,
|
||||
{
|
||||
location_description: data.location_description
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteLocation(id: number): Promise<void> {
|
||||
await api.delete(`/a24/locations/${id}`);
|
||||
export async function deleteLocation(
|
||||
locationId: number,
|
||||
companyId: number
|
||||
): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.delete(`/v1/a76/ports/${locationId}?${params.toString()}`);
|
||||
}
|
||||
@@ -5,12 +5,11 @@ export interface Prevalidator {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
// Campos que faltaban según tu modelo Python:
|
||||
customs_prevalidator?: string;
|
||||
patent_prevalidator?: string;
|
||||
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -35,33 +34,41 @@ export interface PrevalidatorListResponse {
|
||||
export async function getPrevalidators(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<PrevalidatorListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
const response = await api.get(`/a76/prevalidators/?${params.toString()}`);
|
||||
const response = await api.get(`/v1/a76/prevalidators/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getPrevalidator(id: number): Promise<Prevalidator> {
|
||||
const response = await api.get(`/a76/prevalidators/${id}`);
|
||||
export async function getPrevalidator(id: number, companyId?: number): Promise<Prevalidator> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/prevalidators/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createPrevalidator(data: PrevalidatorCreate): Promise<Prevalidator> {
|
||||
const response = await api.post('/a76/prevalidators', data);
|
||||
export async function createPrevalidator(data: PrevalidatorCreate, companyId: number): Promise<Prevalidator> {
|
||||
const response = await api.post(`/v1/a76/prevalidators/?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updatePrevalidator(id: number, data: PrevalidatorUpdate): Promise<Prevalidator> {
|
||||
const response = await api.patch(`/a76/prevalidators/${id}`, data);
|
||||
export async function updatePrevalidator(id: number, data: PrevalidatorUpdate, companyId: number): Promise<Prevalidator> {
|
||||
const response = await api.put(`/v1/a76/prevalidators/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deletePrevalidator(id: number): Promise<void> {
|
||||
await api.delete(`/a76/prevalidators/${id}`);
|
||||
}
|
||||
export async function deletePrevalidator(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/prevalidators/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export async function getSignatures(
|
||||
|
||||
export async function getSignature(id: number, companyId: number): Promise<Signature> {
|
||||
const response = await api.get(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -63,6 +64,7 @@ export async function createSignature(
|
||||
companyId: number
|
||||
): Promise<Signature> {
|
||||
const response = await api.post(`/v1/a76/signatures/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -72,9 +74,11 @@ export async function updateSignature(
|
||||
companyId: number
|
||||
): Promise<Signature> {
|
||||
const response = await api.put(`/v1/a76/signatures/${id}/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteSignature(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
const response = await api.delete(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export async function getUnitConversions(
|
||||
|
||||
export async function getUnitConversion(id: number, companyId: number): Promise<UnitConversion> {
|
||||
const response = await api.get(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ export async function createUnitConversion(
|
||||
companyId: number
|
||||
): Promise<UnitConversion> {
|
||||
const response = await api.post(`/v1/a76/unit-conversions/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -69,9 +71,11 @@ export async function updateUnitConversion(
|
||||
companyId: number
|
||||
): Promise<UnitConversion> {
|
||||
const response = await api.put(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteUnitConversion(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
const response = await api.delete(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'integration_number',
|
||||
header: 'No. Integración',
|
||||
cell: ({ row }) => row.original.integration_number || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'pedimentos',
|
||||
header: 'Pedimentos',
|
||||
cell: ({ row }) => row.original.pedimentos || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'doda_date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => row.original.doda_date || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => row.original.status || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -3,99 +3,138 @@
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Tabs from "$lib/components/ui/tabs"; // Necesario para organizar
|
||||
import { Plus, Trash2 } from "lucide-svelte"; // Iconos para la lista
|
||||
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { Switch } from "$lib/components/ui/switch";
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createDoda,
|
||||
updateDoda,
|
||||
type Doda,
|
||||
type DodaContainer,
|
||||
type DodaPedimento,
|
||||
type DodaAmericanPedimento
|
||||
type Doda
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
doda = null,
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
doda?: Doda | null;
|
||||
item?: Doda | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!doda);
|
||||
const title = $derived(isEdit ? `Editar DODA ${doda?.integration_number || ''}` : "Nuevo DODA");
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar DODA ${item?.integration_number || ''}` : "Nuevo DODA");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
integration_number: '',
|
||||
doda_date: undefined as number | undefined,
|
||||
doda_time: undefined as number | undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
// Arrays
|
||||
containers: [] as Partial<DodaContainer>[],
|
||||
pedimentos_detail: [] as Partial<DodaPedimento>[],
|
||||
american_pedimentos: [] as Partial<DodaAmericanPedimento>[]
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined as number | undefined,
|
||||
unique_badge_number: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Helpers
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Manejo de Arrays
|
||||
function addContainer() {
|
||||
formData.containers = [...formData.containers, { container_value: '', seals: '' }];
|
||||
}
|
||||
|
||||
function removeContainer(index: number) {
|
||||
formData.containers = formData.containers.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (doda) {
|
||||
if (item) {
|
||||
formData = {
|
||||
integration_number: doda.integration_number || '',
|
||||
dispatch_customs: doda.dispatch_customs || '',
|
||||
customs_sections: doda.customs_sections || '',
|
||||
patent: doda.patent || '',
|
||||
caat: doda.caat || '',
|
||||
transport_identification: doda.transport_identification || '',
|
||||
fast_id: doda.fast_id || '',
|
||||
operation_type: doda.operation_type || '',
|
||||
containers: doda.containers || [],
|
||||
pedimentos_detail: doda.pedimentos_detail || [],
|
||||
american_pedimentos: doda.american_pedimentos || []
|
||||
integration_number: item.integration_number || '',
|
||||
doda_date: item.doda_date,
|
||||
doda_time: item.doda_time,
|
||||
dispatch_customs: item.dispatch_customs || '',
|
||||
customs_sections: item.customs_sections || '',
|
||||
patent: item.patent || '',
|
||||
pedimentos: item.pedimentos || '',
|
||||
caat: item.caat || '',
|
||||
transport_identification: item.transport_identification || '',
|
||||
fast_id: item.fast_id || '',
|
||||
operation_type: item.operation_type || '',
|
||||
selected: item.selected || false,
|
||||
user_selected: item.user_selected || '',
|
||||
last_user: item.last_user || '',
|
||||
responsible: item.responsible || '',
|
||||
carrier: item.carrier || '',
|
||||
shipments: item.shipments || '',
|
||||
pedimento_type: item.pedimento_type || '',
|
||||
original_chain: item.original_chain || '',
|
||||
serial_number: item.serial_number || '',
|
||||
electronic_signature: item.electronic_signature || '',
|
||||
transaction_number: item.transaction_number || '',
|
||||
status: item.status || '',
|
||||
linq_sat_qr: item.linq_sat_qr || '',
|
||||
sat_certificate: item.sat_certificate || '',
|
||||
sat_digital_seal: item.sat_digital_seal || '',
|
||||
xml_doda_sent_path: item.xml_doda_sent_path || '',
|
||||
xml_doda_response_path: item.xml_doda_response_path || '',
|
||||
sat_original_chain: item.sat_original_chain || '',
|
||||
customs_clearance: item.customs_clearance,
|
||||
unique_badge_number: item.unique_badge_number || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
integration_number: '',
|
||||
doda_date: undefined,
|
||||
doda_time: undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
containers: [],
|
||||
pedimentos_detail: [],
|
||||
american_pedimentos: []
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined,
|
||||
unique_badge_number: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
@@ -103,19 +142,22 @@
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Nota: Aquí validaciones si fueran necesarias
|
||||
|
||||
// Fix para el ID: Usamos id o sys_id según venga
|
||||
const idToUpdate = doda?.sys_id || doda?.id;
|
||||
const idToUpdate = item?.id;
|
||||
|
||||
if (isEdit && idToUpdate) {
|
||||
await updateDoda(idToUpdate, formData);
|
||||
await updateDoda(idToUpdate, formData, companyId);
|
||||
} else {
|
||||
await createDoda(formData);
|
||||
await createDoda(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
@@ -131,12 +173,12 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Content class="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="py-2">
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="py-4">
|
||||
{#if error}
|
||||
<div class="mb-4 rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
@@ -144,122 +186,176 @@
|
||||
{/if}
|
||||
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-3">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Transporte</Tabs.Trigger>
|
||||
<Tabs.Trigger value="containers">
|
||||
Contenedores ({formData.containers.length})
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Aduana/Transp.</Tabs.Trigger>
|
||||
<Tabs.Trigger value="sat">SAT / Digital</Tabs.Trigger>
|
||||
<Tabs.Trigger value="other">Otros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- TAB: GENERAL -->
|
||||
<Tabs.Content value="general" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="integration_number">No. Integración</Label>
|
||||
<Input id="integration_number" bind:value={formData.integration_number} maxlength={30} disabled={loading} />
|
||||
<Input id="integration_number" bind:value={formData.integration_number} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} disabled={loading} />
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_date">Fecha (YYYYMMDD)</Label>
|
||||
<Input type="number" id="doda_date" bind:value={formData.doda_date} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} disabled={loading} />
|
||||
<Label for="doda_time">Hora (HHMMSS)</Label>
|
||||
<Input type="number" id="doda_time" bind:value={formData.doda_time} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="operation_type">Tipo Operación</Label>
|
||||
<select
|
||||
id="operation_type"
|
||||
bind:value={formData.operation_type}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
<option value="1">Importación</option>
|
||||
<option value="2">Exportación</option>
|
||||
</select>
|
||||
<Input id="operation_type" bind:value={formData.operation_type} maxlength={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimentos">Pedimentos</Label>
|
||||
<Input id="pedimentos" bind:value={formData.pedimentos} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento_type">Tipo Pedimento</Label>
|
||||
<Input id="pedimento_type" bind:value={formData.pedimento_type} maxlength={30} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: ADUANA / TRANSPORTE -->
|
||||
<Tabs.Content value="transport" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat">CAAT</Label>
|
||||
<Input id="caat" bind:value={formData.caat} maxlength={10} disabled={loading} />
|
||||
<Input id="caat" bind:value={formData.caat} maxlength={10} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transport_id">Identificación Transporte</Label>
|
||||
<Input id="transport_id" bind:value={formData.transport_identification} maxlength={20} disabled={loading} />
|
||||
<Label for="carrier">Transportista (Carrier)</Label>
|
||||
<Input id="carrier" bind:value={formData.carrier} maxlength={8} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="transport_identification">Ident. Transporte</Label>
|
||||
<Input id="transport_identification" bind:value={formData.transport_identification} maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fast_id">FAST ID</Label>
|
||||
<Input id="fast_id" bind:value={formData.fast_id} maxlength={20} disabled={loading} />
|
||||
<Input id="fast_id" bind:value={formData.fast_id} maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="shipments">Embarques (Shipments)</Label>
|
||||
<Input id="shipments" bind:value={formData.shipments} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_clearance">Despacho Aduanero (ID)</Label>
|
||||
<Input type="number" id="customs_clearance" bind:value={formData.customs_clearance} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: SAT / DIGITAL -->
|
||||
<Tabs.Content value="sat" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="serial_number">Número de Serie</Label>
|
||||
<Input id="serial_number" bind:value={formData.serial_number} maxlength={21} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transaction_number">No. Transacción</Label>
|
||||
<Input id="transaction_number" bind:value={formData.transaction_number} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="unique_badge_number">Número Único de Gafete</Label>
|
||||
<Input id="unique_badge_number" bind:value={formData.unique_badge_number} maxlength={250} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="original_chain">Cadena Original</Label>
|
||||
<Textarea id="original_chain" bind:value={formData.original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="electronic_signature">Firma Electrónica</Label>
|
||||
<Textarea id="electronic_signature" bind:value={formData.electronic_signature} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_digital_seal">Sello Digital SAT</Label>
|
||||
<Textarea id="sat_digital_seal" bind:value={formData.sat_digital_seal} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_original_chain">Cadena Original SAT</Label>
|
||||
<Textarea id="sat_original_chain" bind:value={formData.sat_original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_sent_path">Ruta XML Enviado</Label>
|
||||
<Input id="xml_doda_sent_path" bind:value={formData.xml_doda_sent_path} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_response_path">Ruta XML Respuesta</Label>
|
||||
<Input id="xml_doda_response_path" bind:value={formData.xml_doda_response_path} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="containers" class="space-y-4 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<Label>Lista de Contenedores</Label>
|
||||
<Button type="button" size="sm" variant="outline" onclick={addContainer} disabled={loading}>
|
||||
<Plus class="mr-2 h-3 w-3" /> Agregar
|
||||
</Button>
|
||||
<!-- TAB: OTROS -->
|
||||
<Tabs.Content value="other" class="space-y-4 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch id="selected" bind:checked={formData.selected} />
|
||||
<Label for="selected">Seleccionado</Label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 max-h-[300px] overflow-y-auto pr-1">
|
||||
{#if formData.containers.length === 0}
|
||||
<div class="rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground">
|
||||
No hay contenedores registrados.
|
||||
</div>
|
||||
{:else}
|
||||
{#each formData.containers as container, i}
|
||||
<div class="flex items-end gap-3 rounded-md border p-3 bg-muted/20">
|
||||
<div class="grid gap-1.5 flex-1">
|
||||
<Label class="text-xs">Valor Contenedor</Label>
|
||||
<Input bind:value={container.container_value} placeholder="Ej. ABCD123456" class="h-8" />
|
||||
</div>
|
||||
<div class="grid gap-1.5 flex-1">
|
||||
<Label class="text-xs">Candados</Label>
|
||||
<Input bind:value={container.seals} placeholder="Separados por coma" class="h-8" />
|
||||
</div>
|
||||
<Button type="button" variant="destructive" size="icon" class="h-8 w-8" onclick={() => removeContainer(i)}>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="user_selected">Usuario Selección</Label>
|
||||
<Input id="user_selected" bind:value={formData.user_selected} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last_user">Último Usuario</Label>
|
||||
<Input id="last_user" bind:value={formData.last_user} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="responsible">Responsable</Label>
|
||||
<Input id="responsible" bind:value={formData.responsible} maxlength={14} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="linq_sat_qr">LINQ SAT QR</Label>
|
||||
<Input id="linq_sat_qr" bind:value={formData.linq_sat_qr} maxlength={1000} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_certificate">Certificado SAT</Label>
|
||||
<Input id="sat_certificate" bind:value={formData.sat_certificate} maxlength={2001} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
{#if isEdit && doda}
|
||||
<div class="mt-4 rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(doda.created_at)}</span>
|
||||
</div>
|
||||
{#if doda.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(doda.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer class="mt-6">
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { deleteDoda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Doda;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Doda | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este registro DODA?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteDoda(item.id, companyId);
|
||||
alert('✅ Registro eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting doda:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: pageCount,
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {Number($page.url.searchParams.get('page') || 1)} de {pageCount}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { ElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<ElectronicNotice>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'notice_number',
|
||||
header: 'No. Aviso',
|
||||
cell: ({ row }) => row.original.notice_number || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'year',
|
||||
header: 'Año',
|
||||
cell: ({ row }) => row.original.year || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'pedimento',
|
||||
header: 'Pedimento',
|
||||
cell: ({ row }) => row.original.pedimento || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => row.original.status || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createElectronicNotice,
|
||||
updateElectronicNotice,
|
||||
@@ -12,16 +12,16 @@
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
notice = null,
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
notice?: ElectronicNotice | null;
|
||||
item?: ElectronicNotice | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!notice);
|
||||
const title = $derived(isEdit ? "Editar Aviso Electrónico" : "Nuevo Aviso Electrónico");
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Aviso ${item?.notice_number || ''}` : "Nuevo Aviso Electrónico");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
@@ -41,37 +41,37 @@
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos al abrir
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (notice) {
|
||||
if (item) {
|
||||
formData = {
|
||||
notice_number: notice.notice_number || '',
|
||||
year: notice.year || '',
|
||||
patent: notice.patent || '',
|
||||
pedimento: notice.pedimento || '',
|
||||
invoice: notice.invoice || '',
|
||||
status: notice.status || '',
|
||||
validation_acknowledgment: notice.validation_acknowledgment || '',
|
||||
certificate_number: notice.certificate_number || '',
|
||||
file_sent: notice.file_sent || '',
|
||||
file_response: notice.file_response || '',
|
||||
fea: notice.fea || ''
|
||||
notice_number: item.notice_number || '',
|
||||
year: item.year || '',
|
||||
patent: item.patent || '',
|
||||
pedimento: item.pedimento || '',
|
||||
invoice: item.invoice || '',
|
||||
status: item.status || '',
|
||||
validation_acknowledgment: item.validation_acknowledgment || '',
|
||||
certificate_number: item.certificate_number || '',
|
||||
file_sent: item.file_sent || '',
|
||||
file_response: item.file_response || '',
|
||||
fea: item.fea || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
notice_number: '', year: '', patent: '', pedimento: '', invoice: '',
|
||||
status: '', validation_acknowledgment: '', certificate_number: '',
|
||||
file_sent: '', file_response: '', fea: ''
|
||||
notice_number: '',
|
||||
year: '',
|
||||
patent: '',
|
||||
pedimento: '',
|
||||
invoice: '',
|
||||
status: '',
|
||||
validation_acknowledgment: '',
|
||||
certificate_number: '',
|
||||
file_sent: '',
|
||||
file_response: '',
|
||||
fea: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
@@ -79,26 +79,20 @@
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones básicas
|
||||
if (!formData.notice_number.trim()) throw new Error('El número de aviso es requerido');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
...formData,
|
||||
notice_number: formData.notice_number.trim(),
|
||||
year: formData.year.trim(),
|
||||
patent: formData.patent.trim(),
|
||||
pedimento: formData.pedimento.trim()
|
||||
};
|
||||
|
||||
if (isEdit && notice) {
|
||||
await updateElectronicNotice(notice.id, dataToSend);
|
||||
if (isEdit && item) {
|
||||
await updateElectronicNotice(item.id, formData, companyId);
|
||||
} else {
|
||||
await createElectronicNotice(dataToSend);
|
||||
await createElectronicNotice(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
@@ -106,7 +100,7 @@
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar el aviso';
|
||||
error = e instanceof Error ? e.message : 'Error al guardar aviso electrónico';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -119,7 +113,7 @@
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6 py-4">
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
@@ -127,83 +121,70 @@
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
<div class="col-span-2 border-b pb-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Identificación</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="notice_number">No. Aviso <span class="text-destructive">*</span></Label>
|
||||
<Input id="notice_number" bind:value={formData.notice_number} placeholder="Ej. AV-2025-001" maxlength={500} disabled={loading} required />
|
||||
<Label for="notice_number">No. Aviso</Label>
|
||||
<Input id="notice_number" bind:value={formData.notice_number} placeholder="Ej. 12345" maxlength={500} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="year">Año</Label>
|
||||
<Input id="year" bind:value={formData.year} placeholder="Ej. 2025" maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 border-b pb-2 mt-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Datos Operativos</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} placeholder="Ej. 1234" maxlength={4} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento">Pedimento</Label>
|
||||
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Ej. 5000123" maxlength={15} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="invoice">Factura / Invoice</Label>
|
||||
<Input id="invoice" bind:value={formData.invoice} maxlength={50} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} placeholder="Ej. VALIDADO" maxlength={100} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 border-b pb-2 mt-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Validación</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="val_ack">Acuse Validación</Label>
|
||||
<Input id="val_ack" bind:value={formData.validation_acknowledgment} maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="cert">No. Certificado</Label>
|
||||
<Input id="cert" bind:value={formData.certificate_number} maxlength={50} disabled={loading} />
|
||||
<Input id="year" bind:value={formData.year} placeholder="Ej. 2024" maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isEdit && notice}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(notice.created_at)}</span>
|
||||
</div>
|
||||
{#if notice.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(notice.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} placeholder="Ej. 1234" maxlength={4} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento">Pedimento</Label>
|
||||
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Ej. 1234567" maxlength={15} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="invoice">Factura</Label>
|
||||
<Input id="invoice" bind:value={formData.invoice} placeholder="Ej. F-123" maxlength={50} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} placeholder="Ej. Validado" maxlength={100} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="validation_acknowledgment">Acuse Validación</Label>
|
||||
<Input id="validation_acknowledgment" bind:value={formData.validation_acknowledgment} placeholder="Ej. AC-123" maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="certificate_number">No. Certificado</Label>
|
||||
<Input id="certificate_number" bind:value={formData.certificate_number} placeholder="Ej. CERT-123" maxlength={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="fea">FEA</Label>
|
||||
<Input id="fea" bind:value={formData.fea} placeholder="Firma Electrónica Avanzada" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="file_sent">Archivo Enviado</Label>
|
||||
<Input id="file_sent" bind:value={formData.file_sent} placeholder="Nombre del archivo enviado" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="file_response">Archivo Respuesta</Label>
|
||||
<Input id="file_response" bind:value={formData.file_response} placeholder="Nombre del archivo respuesta" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { ElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { deleteElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: ElectronicNotice;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<ElectronicNotice | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este aviso electrónico?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteElectronicNotice(item.id, companyId);
|
||||
alert('✅ Aviso electrónico eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el aviso electrónico';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting electronic notice:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: pageCount,
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Equivalency>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'fraccion_mex',
|
||||
header: 'Fracción MX',
|
||||
cell: ({ row }) => row.original.fraccion_mex || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'fraccion_us',
|
||||
header: 'Fracción US',
|
||||
cell: ({ row }) => row.original.fraccion_us || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createEquivalency,
|
||||
updateEquivalency,
|
||||
type Equivalency
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Equivalency | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Equivalencia ${item?.fraccion_mex || ''}` : "Nueva Equivalencia");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
fraccion_mex: '',
|
||||
fraccion_us: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
fraccion_mex: item.fraccion_mex || '',
|
||||
fraccion_us: item.fraccion_us || '',
|
||||
description: item.description || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
fraccion_mex: '',
|
||||
fraccion_us: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (isEdit && item) {
|
||||
response = await updateEquivalency(item.id, formData, companyId);
|
||||
} else {
|
||||
response = await createEquivalency(formData, companyId);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar equivalencia';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_mex" class="text-right">Fracción MX</Label>
|
||||
<Input id="fraccion_mex" bind:value={formData.fraccion_mex} class="col-span-3" maxlength={10} required placeholder="Ej. 8544.11.01" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_us" class="text-right">Fracción US</Label>
|
||||
<Input id="fraccion_us" bind:value={formData.fraccion_us} class="col-span-3" maxlength={100} required placeholder="Ej. 8544.11.00" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="description" class="text-right">Descripción</Label>
|
||||
<Input id="description" bind:value={formData.description} class="col-span-3" maxlength={200} />
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { deleteEquivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Equivalency;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Equivalency | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar esta equivalencia?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await deleteEquivalency(item.id, companyId);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
alert('✅ Equivalencia eliminada correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la equivalencia';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting equivalency:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: pageCount,
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,137 @@
|
||||
<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 { Textarea } from "$lib/components/ui/textarea";
|
||||
import { Loader2 } from "lucide-svelte";
|
||||
import type { Equivalency, EquivalencyCreate } from "$lib/api/dashboard/a76/general_catalogs/equivalencies";
|
||||
|
||||
// --- Props ---
|
||||
export let open = false;
|
||||
export let mode: "create" | "edit" = "create";
|
||||
export let initialData: Equivalency | null = null;
|
||||
|
||||
// Función que el padre debe pasar para realizar la llamada a la API
|
||||
export let onSave: (data: EquivalencyCreate) => Promise<void>;
|
||||
|
||||
// --- Estado Local ---
|
||||
let loading = false;
|
||||
|
||||
// Estado del formulario
|
||||
let formData: EquivalencyCreate = {
|
||||
fraccion_mex: "",
|
||||
fraccion_us: "",
|
||||
description: ""
|
||||
};
|
||||
|
||||
// --- Reactividad ---
|
||||
// Cuando se abre el modal o cambia initialData, reseteamos/llenamos el form
|
||||
$: if (open) {
|
||||
if (mode === "edit" && initialData) {
|
||||
formData = {
|
||||
fraccion_mex: initialData.fraccion_mex,
|
||||
fraccion_us: initialData.fraccion_us,
|
||||
description: initialData.description || ""
|
||||
};
|
||||
} else {
|
||||
// Reset para crear
|
||||
formData = {
|
||||
fraccion_mex: "",
|
||||
fraccion_us: "",
|
||||
description: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// --- Handlers ---
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
// Validaciones simples
|
||||
if (!formData.fraccion_mex || !formData.fraccion_us) {
|
||||
toast.error("Las fracciones son obligatorias");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await onSave(formData);
|
||||
open = false; // Cerramos el modal si todo sale bien
|
||||
toast.success(mode === 'create' ? "Equivalencia creada" : "Equivalencia actualizada");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Error al guardar la equivalencia");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{mode === "create" ? "Nueva Equivalencia" : "Editar Equivalencia"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{mode === "create"
|
||||
? "Ingresa los datos para registrar una nueva equivalencia."
|
||||
: "Modifica los datos de la equivalencia existente."}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_mex" class="text-right">Fracción MX</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="fraccion_mex"
|
||||
bind:value={formData.fraccion_mex}
|
||||
placeholder="Ej. 8544.11.01"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_us" class="text-right">Fracción US</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="fraccion_us"
|
||||
bind:value={formData.fraccion_us}
|
||||
placeholder="Ej. 8544.10.00"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label for="description" class="text-right pt-2">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Descripción opcional de la equivalencia..."
|
||||
class="resize-none"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" on:click={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button on:click={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
Guardar
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { ErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<ErrorCatalog>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description ?? '—'
|
||||
},
|
||||
{
|
||||
accessorKey: 'classification_id',
|
||||
header: 'Clasificación',
|
||||
cell: ({ row }) => row.original.classification_id ?? '—'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<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 * as Select from "$lib/components/ui/select";
|
||||
import {
|
||||
createErrorCatalog,
|
||||
updateErrorCatalog,
|
||||
getErrorClassifications,
|
||||
type ErrorCatalog,
|
||||
type ErrorClassification
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/error-catalogs";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ErrorCatalog | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Error" : "Nuevo Error");
|
||||
|
||||
let formData = $state({
|
||||
code: "",
|
||||
description: "",
|
||||
classification_id: ""
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let classifications = $state<ErrorClassification[]>([]);
|
||||
let loadingClassifications = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code || "",
|
||||
description: item.description || "",
|
||||
classification_id: item.classification_id ? String(item.classification_id) : ""
|
||||
};
|
||||
} else {
|
||||
formData = { code: "", description: "", classification_id: "" };
|
||||
}
|
||||
error = null;
|
||||
loadClassifications();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadClassifications() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
loadingClassifications = true;
|
||||
try {
|
||||
const response = await getErrorClassifications(companyId, 1, 100);
|
||||
classifications = response.items || [];
|
||||
} catch (err) {
|
||||
console.error("Error loading classifications", err);
|
||||
} finally {
|
||||
loadingClassifications = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error("No hay una compañía seleccionada");
|
||||
|
||||
if (!formData.code.trim()) throw new Error("El código es requerido");
|
||||
|
||||
const basePayload = {
|
||||
description: formData.description?.trim() || null,
|
||||
classification_id: formData.classification_id ? Number(formData.classification_id) : null
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateErrorCatalog(item.id, basePayload, companyId);
|
||||
alert("✅ Error actualizado correctamente");
|
||||
} else {
|
||||
const createPayload = {
|
||||
code: formData.code.trim(),
|
||||
...basePayload
|
||||
};
|
||||
await createErrorCatalog(createPayload, companyId);
|
||||
alert("✅ Error creado correctamente");
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[520px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="code" bind:value={formData.code} maxlength={15} disabled={loading || isEdit} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="description" class="text-right">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="description" bind:value={formData.description} maxlength={255} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="classification" class="text-right">Clasificación</Label>
|
||||
<div class="col-span-3">
|
||||
<Select.Root bind:value={formData.classification_id} disabled={loading || loadingClassifications}>
|
||||
<Select.Trigger>
|
||||
{#if formData.classification_id}
|
||||
{#each classifications as classification (classification.id)}
|
||||
{#if String(classification.id) === formData.classification_id}
|
||||
{classification.code}{#if classification.level} - {classification.level}{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-muted-foreground">
|
||||
{loadingClassifications ? "Cargando..." : "Seleccione una clasificación"}
|
||||
</span>
|
||||
{/if}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">Sin clasificación</Select.Item>
|
||||
{#each classifications as classification (classification.id)}
|
||||
<Select.Item value={String(classification.id)}>
|
||||
{classification.code}{#if classification.level} - {classification.level}{/if}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -1,165 +0,0 @@
|
||||
<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 {
|
||||
createErrorClassification,
|
||||
updateErrorClassification,
|
||||
type ErrorClassification
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/error-catalogs'; // Ajusta la ruta si es necesario
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
classification = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
classification?: ErrorClassification | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!classification);
|
||||
const title = $derived(isEdit ? "Editar Clasificación" : "Nueva Clasificación");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
level: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas (solo visualización)
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos al abrir o cambiar el item
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (classification) {
|
||||
formData = {
|
||||
code: classification.code,
|
||||
level: classification.level || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
level: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones básicas
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
level: formData.level.trim()
|
||||
};
|
||||
|
||||
if (isEdit && classification) {
|
||||
// En edición, solo mandamos el nivel según tu lógica original (código bloqueado)
|
||||
await updateErrorClassification(classification.id, { level: dataToSend.level });
|
||||
} else {
|
||||
await createErrorClassification(dataToSend);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la clasificación';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[450px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej. SYSTEM_ERROR"
|
||||
maxlength={100}
|
||||
disabled={isEdit || loading}
|
||||
required
|
||||
/>
|
||||
{#if isEdit}
|
||||
<p class="text-[10px] text-muted-foreground mt-1">El código no se puede modificar.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="level" class="text-right">Nivel</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="level"
|
||||
bind:value={formData.level}
|
||||
placeholder="Ej. CRT"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 3 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isEdit && classification}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(classification.created_at)}</span>
|
||||
</div>
|
||||
{#if classification.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(classification.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { ErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { deleteErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: ErrorCatalog;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<ErrorCatalog | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de que desea eliminar este error?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteErrorCatalog(item.id, companyId);
|
||||
alert('✅ Error eliminado correctamente');
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el error';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function handleDialogSuccess() {
|
||||
dialogOpen = false;
|
||||
selectedItem = null;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-[160px]">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={handleDialogSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Identifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Identifier>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Clave',
|
||||
cell: ({ row }) => row.original.code || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'level',
|
||||
header: 'Nivel',
|
||||
cell: ({ row }) => row.original.level || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'complement',
|
||||
header: 'Complemento',
|
||||
cell: ({ row }) => row.original.complement || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createIdentifier,
|
||||
updateIdentifier,
|
||||
type Identifier
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Identifier | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Identificador ${item?.code || ''}` : "Nuevo Identificador");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: '',
|
||||
level: '',
|
||||
complement: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
level: item.level || '',
|
||||
complement: item.complement || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
code: '',
|
||||
description: '',
|
||||
level: '',
|
||||
complement: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
if (isEdit && item) {
|
||||
await updateIdentifier(item.id, formData, companyId);
|
||||
} else {
|
||||
await createIdentifier(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar identificador';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="code">Clave</Label>
|
||||
<Input id="code" bind:value={formData.code} placeholder="Ej. AI" maxlength={2} required />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Descripción</Label>
|
||||
<Input id="description" bind:value={formData.description} placeholder="Descripción del identificador" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="level">Nivel</Label>
|
||||
<Input id="level" bind:value={formData.level} placeholder="Ej. G" maxlength={1} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="complement">Complemento</Label>
|
||||
<Input id="complement" bind:value={formData.complement} placeholder="Información complementaria" maxlength={5000} />
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Identifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
import { deleteIdentifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Identifier;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Identifier | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este identificador?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteIdentifier(item.id, companyId);
|
||||
alert('✅ Identificador eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el identificador';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting identifier:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: pageCount,
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<INPC>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'year',
|
||||
header: 'Año',
|
||||
cell: ({ row }) => row.original.year || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'month',
|
||||
header: 'Mes',
|
||||
cell: ({ row }) => row.original.month || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: 'Valor',
|
||||
cell: ({ row }) => row.original.value?.toString() || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createINPC,
|
||||
updateINPC,
|
||||
type INPC
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: INPC | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar INPC ${item?.year}-${item?.month}` : "Nuevo INPC");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
year: '',
|
||||
month: '',
|
||||
value: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
year: item.year || '',
|
||||
month: item.month || '',
|
||||
value: item.value?.toString() || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
year: '',
|
||||
month: '',
|
||||
value: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
year: formData.year,
|
||||
month: formData.month,
|
||||
value: formData.value ? parseFloat(formData.value) : undefined
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateINPC(item.id, payload, companyId);
|
||||
} else {
|
||||
await createINPC(payload, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar INPC';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="year" class="text-right">Año</Label>
|
||||
<Input id="year" bind:value={formData.year} class="col-span-3" maxlength={4} required />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="month" class="text-right">Mes</Label>
|
||||
<Input id="month" bind:value={formData.month} class="col-span-3" maxlength={2} required placeholder="01-12" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor</Label>
|
||||
<Input id="value" type="number" step="0.00000001" bind:value={formData.value} class="col-span-3" />
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { deleteINPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: INPC;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<INPC | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este registro de INPC?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteINPC(item.id, companyId);
|
||||
alert('✅ Registro eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting INPC:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: pageCount,
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,126 @@
|
||||
<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 {
|
||||
createLocation,
|
||||
updateLocation,
|
||||
type Location
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/locations";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Location | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Ubicación" : "Nueva Ubicación");
|
||||
|
||||
let formData = $state({
|
||||
location_code: "",
|
||||
location_description: ""
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
location_code: item.location_code || "",
|
||||
location_description: item.location_description || ""
|
||||
};
|
||||
} else {
|
||||
formData = { location_code: "", location_description: "" };
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error("No hay una compañía seleccionada");
|
||||
|
||||
if (!formData.location_code.trim()) throw new Error("El código es requerido");
|
||||
|
||||
const basePayload = {
|
||||
location_description: formData.location_description?.trim() || null
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateLocation(item.id, basePayload, companyId);
|
||||
alert("✅ Ubicación actualizada correctamente");
|
||||
} else {
|
||||
const createPayload = {
|
||||
location_code: formData.location_code.trim(),
|
||||
...basePayload
|
||||
};
|
||||
await createLocation(createPayload, companyId);
|
||||
alert("✅ Ubicación creada correctamente");
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[520px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="location_code" class="text-right">Código *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="location_code" bind:value={formData.location_code} maxlength={4} disabled={loading || isEdit} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="location_description" class="text-right">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="location_description" bind:value={formData.location_description} maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -1,166 +0,0 @@
|
||||
<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";
|
||||
// Si tienes el componente Textarea impórtalo, si no, usa la etiqueta html con clases
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
|
||||
import {
|
||||
createLocation,
|
||||
updateLocation,
|
||||
type Location
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/locations';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
location = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
location?: Location | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!location);
|
||||
const title = $derived(isEdit ? "Editar Ubicación" : "Nueva Ubicación");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (location) {
|
||||
formData = {
|
||||
code: location.code,
|
||||
description: location.description || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
if (formData.code.length > 5) throw new Error('El código no puede tener más de 5 caracteres');
|
||||
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
description: formData.description.trim()
|
||||
};
|
||||
|
||||
if (isEdit && location) {
|
||||
await updateLocation(location.id, dataToSend);
|
||||
} else {
|
||||
await createLocation(dataToSend);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la ubicación';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[450px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej. VER"
|
||||
maxlength={5}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 5 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label for="description" class="text-right pt-2">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Descripción de la ubicación..."
|
||||
maxlength={200}
|
||||
disabled={loading}
|
||||
class="resize-none min-h-[80px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isEdit && location}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(location.created_at)}</span>
|
||||
</div>
|
||||
{#if location.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(location.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Prevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Prevalidator>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'customs_prevalidator',
|
||||
header: 'Aduana',
|
||||
cell: ({ row }) => row.original.customs_prevalidator || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent_prevalidator',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent_prevalidator || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -3,26 +3,25 @@
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Textarea } from "$lib/components/ui/textarea"; // Asegúrate de tener este componente o usa <textarea class="...">
|
||||
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createPrevalidator,
|
||||
updatePrevalidator,
|
||||
type Prevalidator
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
prevalidator = null,
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
prevalidator?: Prevalidator | null;
|
||||
item?: Prevalidator | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!prevalidator);
|
||||
const title = $derived(isEdit ? "Editar Prevalidador" : "Nuevo Prevalidador");
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Prevalidador ${item?.code || ''}` : "Nuevo Prevalidador");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
@@ -35,26 +34,18 @@
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos al abrir
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (prevalidator) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: prevalidator.code,
|
||||
description: prevalidator.description || '',
|
||||
customs_prevalidator: prevalidator.customs_prevalidator || '',
|
||||
patent_prevalidator: prevalidator.patent_prevalidator || ''
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
customs_prevalidator: item.customs_prevalidator || '',
|
||||
patent_prevalidator: item.patent_prevalidator || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
code: '',
|
||||
description: '',
|
||||
@@ -67,25 +58,20 @@
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
if (formData.code.length > 20) throw new Error('El código excede los 20 caracteres');
|
||||
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
description: formData.description.trim(),
|
||||
customs_prevalidator: formData.customs_prevalidator.trim(),
|
||||
patent_prevalidator: formData.patent_prevalidator.trim()
|
||||
};
|
||||
|
||||
if (isEdit && prevalidator) {
|
||||
await updatePrevalidator(prevalidator.id, dataToSend);
|
||||
if (isEdit && item) {
|
||||
await updatePrevalidator(item.id, formData, companyId);
|
||||
} else {
|
||||
await createPrevalidator(dataToSend);
|
||||
await createPrevalidator(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
@@ -93,7 +79,7 @@
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar el prevalidador';
|
||||
error = e instanceof Error ? e.message : 'Error al guardar prevalidador';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -106,95 +92,40 @@
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej. PREVAL_01"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 20 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="code">Código</Label>
|
||||
<Input id="code" bind:value={formData.code} placeholder="Ej. 123" maxlength={20} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="customs" class="text-right">Aduana</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="customs"
|
||||
bind:value={formData.customs_prevalidator}
|
||||
placeholder="Ej. 240"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Descripción</Label>
|
||||
<Input id="description" bind:value={formData.description} placeholder="Descripción del prevalidador" maxlength={50} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="patent" class="text-right">Patente</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="patent"
|
||||
bind:value={formData.patent_prevalidator}
|
||||
placeholder="Ej. 1234"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_prevalidator">Aduana</Label>
|
||||
<Input id="customs_prevalidator" bind:value={formData.customs_prevalidator} placeholder="Ej. 123" maxlength={20} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label for="description" class="text-right pt-2">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Descripción breve..."
|
||||
maxlength={50}
|
||||
disabled={loading}
|
||||
class="resize-none min-h-[80px]"
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 50 caracteres</p>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent_prevalidator">Patente</Label>
|
||||
<Input id="patent_prevalidator" bind:value={formData.patent_prevalidator} placeholder="Ej. 1234" maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isEdit && prevalidator}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(prevalidator.created_at)}</span>
|
||||
</div>
|
||||
{#if prevalidator.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(prevalidator.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Prevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { deletePrevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Prevalidator;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Prevalidator | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este prevalidador?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deletePrevalidator(item.id, companyId);
|
||||
alert('✅ Prevalidador eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el prevalidador';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting prevalidator:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: pageCount,
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {Number($page.url.searchParams.get('page') || 1)} de {pageCount}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
|
||||
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Signature } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Signature>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'signature',
|
||||
header: 'Firma',
|
||||
cell: ({ row }) => row.original.signature ?? 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'photo_path',
|
||||
header: 'Ruta Foto',
|
||||
cell: ({ row }) => row.original.photo_path ?? 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Signature } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { deleteSignature } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Signature;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Signature | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar esta firma electrónica?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteSignature(item.id, companyId);
|
||||
alert('✅ Firma eliminada correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la firma';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting signature:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function handleDialogSuccess() {
|
||||
dialogOpen = false;
|
||||
selectedItem = null;
|
||||
if (onSuccess) onSuccess();
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-[160px]">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={handleDialogSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { UnitConversion } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<UnitConversion>[] {
|
||||
@@ -18,15 +19,11 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitConversion>
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return {
|
||||
component: DataTableActions,
|
||||
props: {
|
||||
conversion: row.original,
|
||||
onSuccess
|
||||
}
|
||||
};
|
||||
}
|
||||
cell: ({ row }) =>
|
||||
renderComponent(DataTableActions, {
|
||||
conversion: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Location } from '$lib/api/dashboard/a76/general_catalogs/locations';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export interface Location {
|
||||
location_code: string;
|
||||
location_description: string | null;
|
||||
}
|
||||
|
||||
export function createColumns(): ColumnDef<Location>[] {
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Location>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'location_code',
|
||||
@@ -14,7 +12,15 @@ export function createColumns(): ColumnDef<Location>[] {
|
||||
{
|
||||
accessorKey: 'location_description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.location_description || '-'
|
||||
cell: ({ row }) => row.original.location_description || '—'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Location } from '$lib/api/dashboard/a76/general_catalogs/locations';
|
||||
import { deleteLocation } from '$lib/api/dashboard/a76/general_catalogs/locations';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from '../general_catalogs/locations/create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Location;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Location | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de que desea eliminar esta ubicación?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteLocation(item.id, companyId);
|
||||
alert('✅ Ubicación eliminada correctamente');
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la ubicación';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function handleDialogSuccess() {
|
||||
dialogOpen = false;
|
||||
selectedItem = null;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-[160px]">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={handleDialogSuccess}
|
||||
/>
|
||||
@@ -12,14 +12,23 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', dodas: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const code = url.searchParams.get('code');
|
||||
const description = url.searchParams.get('description');
|
||||
const integrationNumber = url.searchParams.get('integration_number');
|
||||
|
||||
if (code) filters.code = code;
|
||||
if (description) filters.description = description;
|
||||
if (integrationNumber) filters.integration_number = integrationNumber;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
const response = await authenticatedFetch(`v1/a76/doda?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/doda/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/doda/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let searchIntegration = $state($page.url.searchParams.get('integration_number') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'code', label: 'Código' },
|
||||
{ key: 'description', label: 'Descripción' },
|
||||
];
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
if (searchIntegration) url.searchParams.set('integration_number', searchIntegration);
|
||||
else url.searchParams.delete('integration_number');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
@@ -48,7 +40,7 @@
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestión del catálogo de DODA
|
||||
Gestión de Documentos de Operación de Aduana
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
@@ -60,31 +52,24 @@
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por código..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
placeholder="Buscar por No. Integración..."
|
||||
bind:value={searchIntegration}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
<DataTable
|
||||
data={data.dodas?.items || []}
|
||||
columns={columns}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.dodas?.pages || 0}
|
||||
totalItems={data.dodas?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateDialog
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
on:success={handleSuccess}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,21 +12,38 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', notices: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const code = url.searchParams.get('code');
|
||||
const description = url.searchParams.get('description');
|
||||
const notice_number = url.searchParams.get('notice_number');
|
||||
const pedimento = url.searchParams.get('pedimento');
|
||||
|
||||
if (code) filters.code = code;
|
||||
if (description) filters.description = description;
|
||||
if (notice_number) filters.notice_number = notice_number;
|
||||
if (pedimento) filters.pedimento = pedimento;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
const response = await authenticatedFetch(`v1/a76/electronic-notices?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'Failed to load', notices: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
}
|
||||
|
||||
return { notices: await response.json() };
|
||||
const noticesData = await response.json();
|
||||
// Calculate pages if not present
|
||||
if (noticesData.pages === undefined && noticesData.total !== undefined && noticesData.page_size !== undefined) {
|
||||
noticesData.pages = Math.ceil(noticesData.total / noticesData.page_size);
|
||||
}
|
||||
|
||||
return { notices: noticesData };
|
||||
} catch (error) {
|
||||
console.error('Error loading electronic notices:', error);
|
||||
return { error: 'Error loading', notices: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
|
||||
@@ -1,36 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/electronic-notices/create-edit-dialog.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/electronic-notices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic-notices/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic-notices/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let searchNotice = $state($page.url.searchParams.get('notice_number') || '');
|
||||
let searchPedimento = $state($page.url.searchParams.get('pedimento') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'code', label: 'Código' },
|
||||
{ key: 'description', label: 'Descripción' },
|
||||
];
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
if (searchNotice) url.searchParams.set('notice_number', searchNotice);
|
||||
else url.searchParams.delete('notice_number');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
if (searchPedimento) url.searchParams.set('pedimento', searchPedimento);
|
||||
else url.searchParams.delete('pedimento');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
@@ -60,32 +56,31 @@
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por código..."
|
||||
bind:value={searchCode}
|
||||
placeholder="Buscar por No. Aviso..."
|
||||
bind:value={searchNotice}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
placeholder="Buscar por Pedimento..."
|
||||
bind:value={searchPedimento}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
<DataTable
|
||||
data={data.notices?.items || []}
|
||||
columns={columns}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.notices?.pages || 0}
|
||||
totalItems={data.notices?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateDialog
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
on:success={handleSuccess}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -12,21 +12,40 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', equivalencies: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const fraccion_mex = url.searchParams.get('fraccion_mex');
|
||||
const fraccion_us = url.searchParams.get('fraccion_us');
|
||||
const identifier = url.searchParams.get('identifier');
|
||||
const description = url.searchParams.get('description');
|
||||
|
||||
if (fraccion_mex) filters.fraccion_mex = fraccion_mex;
|
||||
if (fraccion_us) filters.fraccion_us = fraccion_us;
|
||||
if (identifier) filters.identifier = identifier;
|
||||
if (description) filters.description = description;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const response = await authenticatedFetch(`v1/a76/equivalencies?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await authenticatedFetch(`v1/a76/equivalencies/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'Failed to load', equivalencies: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
}
|
||||
|
||||
return { equivalencies: await response.json() };
|
||||
const data = await response.json();
|
||||
|
||||
// Calculate pages if not present
|
||||
if (data.pages === undefined && data.total !== undefined && data.page_size !== undefined) {
|
||||
data.pages = Math.ceil(data.total / data.page_size);
|
||||
}
|
||||
|
||||
return { equivalencies: data };
|
||||
} catch (error) {
|
||||
console.error('Error loading equivalencies:', error);
|
||||
return { error: 'Error loading', equivalencies: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/equivalencies/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/equivalencies/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/equivalencies/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchMex = $state($page.url.searchParams.get('fraccion_mex') || '');
|
||||
let searchUS = $state($page.url.searchParams.get('fraccion_us') || '');
|
||||
let searchFraccion = $state($page.url.searchParams.get('fraccion_mex') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'fraccion_mex', label: 'Fracción MEX' },
|
||||
{ key: 'fraccion_us', label: 'Fracción US' },
|
||||
{ key: 'description', label: 'Descripción' },
|
||||
];
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchMex) url.searchParams.set('fraccion_mex', searchMex);
|
||||
if (searchFraccion) url.searchParams.set('fraccion_mex', searchFraccion);
|
||||
else url.searchParams.delete('fraccion_mex');
|
||||
|
||||
if (searchUS) url.searchParams.set('fraccion_us', searchUS);
|
||||
else url.searchParams.delete('fraccion_us');
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
@@ -46,12 +44,12 @@
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Equivalencias Arancelarias</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Equivalencias</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestión del catálogo de equivalencias entre fracciones arancelarias
|
||||
Catálogo de equivalencias
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Equivalencia
|
||||
</Button>
|
||||
@@ -60,26 +58,31 @@
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por fracción MEX..."
|
||||
bind:value={searchMex}
|
||||
placeholder="Buscar por fracción MX..."
|
||||
bind:value={searchFraccion}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por fracción US..."
|
||||
bind:value={searchUS}
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
data={data.equivalencies?.items || []}
|
||||
columns={columns}
|
||||
pageCount={data.equivalencies?.pages || 0}
|
||||
totalItems={data.equivalencies?.total || 0}
|
||||
<DataTable
|
||||
data={data.equivalencies.items}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.equivalencies.pages}
|
||||
totalItems={data.equivalencies.total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PageServerLoad } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
await parent();
|
||||
const parentData = await parent();
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
if (!accessToken) {
|
||||
@@ -18,9 +18,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
|
||||
if (code) filters.code = code;
|
||||
if (description) filters.description = description;
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const response = await authenticatedFetch(`v1/a76/error-catalogs?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No company selected',
|
||||
errors: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
const response = await authenticatedFetch(`v1/a76/error-catalogs/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'Failed to load', errors: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/error_catalogs/create-edite-dialoge.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/error_catalogs/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/error_catalogs/data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/error_catalogs/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
||||
let { data } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
@@ -17,11 +18,6 @@
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'code', label: 'Código' },
|
||||
{ key: 'description', label: 'Descripción' },
|
||||
];
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
@@ -42,7 +38,6 @@
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
@@ -77,18 +72,16 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
<DataTable
|
||||
data={data.errors?.items || []}
|
||||
columns={columns}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.errors?.pages || 0}
|
||||
totalItems={data.errors?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateDialog
|
||||
open={dialogOpen}
|
||||
on:close={() => dialogOpen = false}
|
||||
on:success={handleSuccess}
|
||||
/>
|
||||
|
||||
<CreateDialog
|
||||
bind:open={dialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,70 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.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 { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
// Exchange rate usually has date filter, but for consistency with others I'll check what columns it has.
|
||||
// It has date, value, local_currency.
|
||||
// I'll add a date filter if possible, or just leave it without filters if not applicable easily,
|
||||
// but the pattern requires filters. Maybe filter by local_currency?
|
||||
// Let's check the columns again.
|
||||
// columns.ts has: date, value, local_currency.
|
||||
|
||||
let searchCurrency = $state($page.url.searchParams.get('local_currency') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = createColumns(() => {
|
||||
refreshData();
|
||||
});
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCurrency) url.searchParams.set('local_currency', searchCurrency);
|
||||
else url.searchParams.delete('local_currency');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
loading = true;
|
||||
// Si no estamos en la página 1, navegar a ella
|
||||
const currentPage = Number($page.url.searchParams.get('page') || 1);
|
||||
if (currentPage !== 1) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
await goto(url, { keepFocus: true, noScroll: true });
|
||||
} else {
|
||||
await invalidateAll();
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Tipos de Cambio</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de tipos de cambio
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Tipo de Cambio
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Cambio</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de tipos de cambio
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Tipo de Cambio
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-0">
|
||||
<DataTable
|
||||
data={data.exchange_rates?.items || []}
|
||||
{columns}
|
||||
pageCount={data.exchange_rates?.pages || 0}
|
||||
totalItems={data.exchange_rates?.total || 0}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por moneda..."
|
||||
bind:value={searchCurrency}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={refreshData}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.exchange_rates?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.exchange_rates?.pages || 0}
|
||||
totalItems={data.exchange_rates?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
@@ -1,56 +1,53 @@
|
||||
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
await parent();
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
|
||||
if (!accessToken) {
|
||||
throw redirect(302, '/login');
|
||||
return { error: 'No authenticated', identifiers: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('page_size')) || 50;
|
||||
|
||||
const apiUrl = getServerApiUrl();
|
||||
const endpoint = `${apiUrl}api/v1/a76/identifiers?page=${page}&page_size=${pageSize}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', identifiers: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const code = url.searchParams.get('code');
|
||||
const description = url.searchParams.get('description');
|
||||
|
||||
if (code) filters.code = code;
|
||||
if (description) filters.description = description;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await authenticatedFetch(`v1/a76/identifiers?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error fetching identifiers: ${response.status} ${response.statusText}`);
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
pages: 0,
|
||||
error: `Error: ${response.statusText}`
|
||||
};
|
||||
return { error: 'Failed to load', identifiers: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
items: data.items,
|
||||
total: data.total,
|
||||
page: data.page,
|
||||
pageSize: data.page_size,
|
||||
pages: data.pages
|
||||
};
|
||||
|
||||
// Calculate pages if not present (generic backend might not return it)
|
||||
if (data.pages === undefined && data.total !== undefined && data.page_size !== undefined) {
|
||||
data.pages = Math.ceil(data.total / data.page_size);
|
||||
}
|
||||
|
||||
return { identifiers: data };
|
||||
} catch (error) {
|
||||
console.error('Error fetching identifiers:', error);
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
pages: 0,
|
||||
error: 'Failed to connect to server'
|
||||
};
|
||||
console.error('Error loading identifiers:', error);
|
||||
return { error: 'Error loading', identifiers: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,61 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { createColumns } from '$lib/components/dashboard/identifiers/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/identifiers/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/identifiers/data-table.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 { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/identifiers/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/identifiers/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/identifiers/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = createColumns(() => {
|
||||
refreshData();
|
||||
});
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
}
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Identificadores</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de identificadores
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Identificadores</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de identificadores
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Identificador
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-0">
|
||||
<DataTable
|
||||
data={data.items}
|
||||
{columns}
|
||||
pageCount={data.pages}
|
||||
totalItems={data.total}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={refreshData}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.identifiers.items}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.identifiers.pages}
|
||||
totalItems={data.identifiers.total}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,18 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
if (!accessToken) {
|
||||
return { error: 'No authenticated', inpcs: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
return { error: 'No authenticated', inpc: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', inpc: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const year = url.searchParams.get('year');
|
||||
const month = url.searchParams.get('month');
|
||||
@@ -19,16 +25,29 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
if (year) filters.year = year;
|
||||
if (month) filters.month = month;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const response = await authenticatedFetch(`v1/a76/inpc?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await authenticatedFetch(`v1/a76/inpc/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'Failed to load', inpcs: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
return { error: 'Failed to load', inpc: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
}
|
||||
|
||||
return { inpcs: await response.json() };
|
||||
const data = await response.json();
|
||||
|
||||
// Calculate pages if not present
|
||||
if (data.pages === undefined && data.total !== undefined && data.page_size !== undefined) {
|
||||
data.pages = Math.ceil(data.total / data.page_size);
|
||||
}
|
||||
|
||||
return { inpc: data };
|
||||
} catch (error) {
|
||||
console.error('Error loading INPCs:', error);
|
||||
return { error: 'Error loading', inpcs: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
console.error('Error loading INPC:', error);
|
||||
return { error: 'Error loading', inpc: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edite-dialoge.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/inpc/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/inpc/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/inpc/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchYear = $state($page.url.searchParams.get('year') || '');
|
||||
let searchMonth = $state($page.url.searchParams.get('month') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'year', label: 'Año' },
|
||||
{ key: 'month', label: 'Mes' },
|
||||
{ key: 'value', label: 'Valor' },
|
||||
];
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
@@ -49,10 +46,10 @@
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">INPC</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestión del catálogo de Índice Nacional de Precios al Consumidor
|
||||
Índice Nacional de Precios al Consumidor
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo INPC
|
||||
</Button>
|
||||
@@ -76,16 +73,16 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
data={data.inpcs?.items || []}
|
||||
columns={columns}
|
||||
pageCount={data.inpcs?.pages || 0}
|
||||
totalItems={data.inpcs?.total || 0}
|
||||
<DataTable
|
||||
data={data.inpc.items}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.inpc.pages}
|
||||
totalItems={data.inpc.total}
|
||||
/>
|
||||
|
||||
<CreateDialog
|
||||
bind:open={dialogOpen}
|
||||
on:success={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -72,12 +72,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={data.legends?.items || []}
|
||||
columns={columns}
|
||||
pageCount={data.legends?.pages || 0}
|
||||
totalItems={data.legends?.total || 0}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.legends?.items || []}
|
||||
columns={columns}
|
||||
pageCount={data.legends?.pages || 0}
|
||||
totalItems={data.legends?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateDialog
|
||||
bind:open={dialogOpen}
|
||||
|
||||
@@ -1,71 +1,69 @@
|
||||
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const parentData = await parent();
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
if (!accessToken) {
|
||||
throw redirect(302, '/login');
|
||||
return {
|
||||
error: 'No authenticated',
|
||||
locations: { items: [], total: 0, page: 1, page_size: 10, pages: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('page_size')) || 1000;
|
||||
|
||||
const apiUrl = getServerApiUrl();
|
||||
// Get locations from ports
|
||||
const endpoint = `${apiUrl}api/v1/a76/ports?page=${page}&page_size=${pageSize}`;
|
||||
const pageSize = Number(url.searchParams.get('page_size')) || 50;
|
||||
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No company selected',
|
||||
locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const location_code = url.searchParams.get('location_code');
|
||||
const location_description = url.searchParams.get('location_description');
|
||||
|
||||
if (location_code) filters.location_code = location_code;
|
||||
if (location_description) filters.location_description = location_description;
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/ports/?${queryParams.toString()}`,
|
||||
{ method: 'GET', cache: 'no-store' },
|
||||
cookies,
|
||||
fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error fetching Ports for locations: ${response.status} ${response.statusText}`);
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize,
|
||||
pages: 0,
|
||||
error: `Error: ${response.statusText}`
|
||||
error: 'Failed to load',
|
||||
locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Extract unique locations
|
||||
const locationMap = new Map();
|
||||
data.items.forEach((port: any) => {
|
||||
if (port.location_code && !locationMap.has(port.location_code)) {
|
||||
locationMap.set(port.location_code, {
|
||||
location_code: port.location_code,
|
||||
location_description: port.location_description
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const locations = Array.from(locationMap.values());
|
||||
|
||||
return {
|
||||
items: locations,
|
||||
total: locations.length,
|
||||
page: 1,
|
||||
pageSize: locations.length,
|
||||
pages: 1
|
||||
};
|
||||
return { locations: data };
|
||||
} catch (error) {
|
||||
console.error('Error fetching Locations:', error);
|
||||
console.error('Error loading locations:', error);
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize,
|
||||
pages: 0,
|
||||
error: 'Error al cargar datos'
|
||||
error: 'Error loading',
|
||||
locations: { items: [], total: 0, page: 1, page_size: pageSize, pages: 0 }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,60 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/locations/columns';
|
||||
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edite-dialog.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let dialogOpen = $state(false);
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
const columns = createColumns();
|
||||
let searchCode = $state($page.url.searchParams.get('location_code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('location_description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
async function refreshData() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('location_code', searchCode);
|
||||
else url.searchParams.delete('location_code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('location_description', searchDesc);
|
||||
else url.searchParams.delete('location_description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Ubicaciones</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Ubicaciones</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de ubicaciones extraídas de puertos
|
||||
Catálogo de ubicaciones de puertos
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Localización
|
||||
</Button>
|
||||
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Ubicación
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por código..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-0">
|
||||
<DataTable
|
||||
data={data.items}
|
||||
{columns}
|
||||
pageCount={data.pages}
|
||||
totalItems={data.total}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.locations?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.locations?.pages || 0}
|
||||
totalItems={data.locations?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateDialog
|
||||
bind:open={dialogOpen}
|
||||
on:success={refreshData}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -12,6 +12,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', prevalidators: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const code = url.searchParams.get('code');
|
||||
const description = url.searchParams.get('description');
|
||||
@@ -19,7 +25,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
if (code) filters.code = code;
|
||||
if (description) filters.description = description;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
const response = await authenticatedFetch(`v1/a76/prevalidators?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw, Plus, House } from 'lucide-svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/prevalidators/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/prevalidators/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte';
|
||||
import * as Breadcrumb from "$lib/components/ui/breadcrumb";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar";
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let loading = $state(false);
|
||||
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'code', label: 'Código' },
|
||||
{ key: 'description', label: 'Descripción' },
|
||||
];
|
||||
const columns = createColumns(refreshData);
|
||||
|
||||
async function refreshData() {
|
||||
loading = true;
|
||||
const currentPage = Number($page.url.searchParams.get('page') || 1);
|
||||
if (currentPage !== 1) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
await goto(url, { keepFocus: true, noScroll: true });
|
||||
} else {
|
||||
await invalidateAll();
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
@@ -34,16 +46,40 @@
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
dialogOpen = false;
|
||||
refreshData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<header class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<Sidebar.Trigger class="-ml-1" />
|
||||
<Separator orientation="vertical" class="mr-2 h-4" />
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="/dashboard">
|
||||
<House class="h-4 w-4" />
|
||||
</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block" />
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="/dashboard/general_catalogs">Catálogos Generales</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block" />
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>Prevalidadores</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4 pt-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Prevalidadores</h1>
|
||||
@@ -51,10 +87,15 @@
|
||||
Gestión del catálogo de prevalidadores
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Prevalidador
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
@@ -74,18 +115,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
data={data.prevalidators?.items || []}
|
||||
columns={columns}
|
||||
pageCount={data.prevalidators?.pages || 0}
|
||||
totalItems={data.prevalidators?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
data={data.prevalidators.items}
|
||||
columns={columns}
|
||||
pageCount={data.prevalidators.pages}
|
||||
totalItems={data.prevalidators.total}
|
||||
/>
|
||||
|
||||
<CreateDialog
|
||||
<CreateEditDialog
|
||||
open={dialogOpen}
|
||||
on:close={() => dialogOpen = false}
|
||||
on:success={handleSuccess}
|
||||
onOpenChange={(open) => dialogOpen = open}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,14 +12,18 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return { error: 'No company selected', signatures: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const name = url.searchParams.get('name');
|
||||
const position = url.searchParams.get('position');
|
||||
const code = url.searchParams.get('code');
|
||||
|
||||
if (name) filters.name = name;
|
||||
if (position) filters.position = position;
|
||||
if (code) filters.code = code;
|
||||
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
|
||||
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), company_id: companyId, ...filters });
|
||||
const response = await authenticatedFetch(`v1/a76/signatures?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/signatures/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/signatures/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
let loading = $state(false);
|
||||
|
||||
let searchName = $state($page.url.searchParams.get('name') || '');
|
||||
let searchPosition = $state($page.url.searchParams.get('position') || '');
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Nombre' },
|
||||
{ key: 'position', label: 'Cargo' },
|
||||
{ key: 'certificate', label: 'Certificado' },
|
||||
];
|
||||
const columns = createColumns(refreshData);
|
||||
|
||||
async function refreshData() {
|
||||
loading = true;
|
||||
const currentPage = Number($page.url.searchParams.get('page') || 1);
|
||||
if (currentPage !== 1) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', '1');
|
||||
await goto(url, { keepFocus: true, noScroll: true });
|
||||
} else {
|
||||
await invalidateAll();
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchName) url.searchParams.set('name', searchName);
|
||||
else url.searchParams.delete('name');
|
||||
|
||||
if (searchPosition) url.searchParams.set('position', searchPosition);
|
||||
else url.searchParams.delete('position');
|
||||
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
dialogOpen = false;
|
||||
refreshData();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -52,41 +55,36 @@
|
||||
Gestión del catálogo de firmas electrónicas
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Firma
|
||||
</Button>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => dialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Firma
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por nombre..."
|
||||
bind:value={searchName}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por cargo..."
|
||||
bind:value={searchPosition}
|
||||
placeholder="Buscar por código..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<SimpleDataTable
|
||||
data={data.signatures?.items || []}
|
||||
columns={columns}
|
||||
pageCount={data.signatures?.pages || 0}
|
||||
totalItems={data.signatures?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
data={data.signatures?.items || []}
|
||||
{columns}
|
||||
pageCount={data.signatures?.pages || 0}
|
||||
totalItems={data.signatures?.total || 0}
|
||||
/>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
title="Crear Nueva Firma Electrónica"
|
||||
on:success={handleSuccess}
|
||||
bind:open={dialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -18,11 +18,8 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const filters: Record<string, string> = {};
|
||||
|
||||
// Obtener company_id
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
const companyId = cookieCompanyId ? parseInt(cookieCompanyId) : undefined;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
|
||||
@@ -1,61 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.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 { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = createColumns(() => {
|
||||
refreshData();
|
||||
});
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
}
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida ACE</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida ACE
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida ACE</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida ACE
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-0">
|
||||
<DataTable
|
||||
data={data.ace_units?.items || []}
|
||||
{columns}
|
||||
pageCount={data.ace_units?.pages || 0}
|
||||
totalItems={data.ace_units?.total || 0}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={refreshData}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
data={data.ace_units?.items || []}
|
||||
columns={createColumns(handleSuccess)}
|
||||
pageCount={data.ace_units?.pages || 0}
|
||||
totalItems={data.ace_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,61 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/american/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/american/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/american/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 { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/american/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/american/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/american/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = createColumns(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function handleSuccess() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
}
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Americanas</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida Americanas
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida Americanas</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida Americanas
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data.american_units?.items || []}
|
||||
pageCount={data.american_units?.pages || 0}
|
||||
totalItems={data.american_units?.total || 0}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.american_units?.items || []}
|
||||
pageCount={data.american_units?.pages || 0}
|
||||
totalItems={data.american_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,61 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/customs/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 { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Función unificada para refrescar datos
|
||||
async function handleSuccess() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Definimos las columnas pasando el callback de éxito
|
||||
const columns = createColumns(handleSuccess);
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Aduanas MEX</h2>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida Aduanas MEX</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida para aduanas mexicanas
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<DataTable
|
||||
{columns}
|
||||
data={data.customs_units?.items || []}
|
||||
pageCount={data.customs_units?.pages || 0}
|
||||
totalItems={data.customs_units?.total || 0}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.customs_units?.items || []}
|
||||
pageCount={data.customs_units?.pages || 0}
|
||||
totalItems={data.customs_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
@@ -1,61 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/general/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/general/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/general/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 { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/general/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/general/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/general/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = createColumns(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function handleSuccess() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
}
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Generales</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo general de unidades de medida
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida Generales</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo general de unidades de medida
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data.general_units?.items || []}
|
||||
pageCount={data.general_units?.pages || 0}
|
||||
totalItems={data.general_units?.total || 0}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.general_units?.items || []}
|
||||
pageCount={data.general_units?.pages || 0}
|
||||
totalItems={data.general_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,61 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/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 { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/columns';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/units_of_measure/oma/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let loading = $state(false);
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const columns = createColumns(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function handleSuccess() {
|
||||
loading = true;
|
||||
await invalidateAll();
|
||||
loading = false;
|
||||
}
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida OMA</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida OMA
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
|
||||
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
</Button>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Unidades de Medida OMA</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de unidades de medida OMA
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Unidad
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data.oma_units?.items || []}
|
||||
pageCount={data.oma_units?.pages || 0}
|
||||
totalItems={data.oma_units?.total || 0}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.oma_units?.items || []}
|
||||
pageCount={data.oma_units?.pages || 0}
|
||||
totalItems={data.oma_units?.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,169 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { incotermsApi, type Incoterm } from '$lib/api/dashboard/refrence_data/incoterms';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns.js';
|
||||
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';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns.js';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte';
|
||||
import CreateEditDialog from '$lib/components/dashboard/reference_data/incoterms/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Estado para el diálogo de crear
|
||||
let showCreateDialog = $state(false);
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
let createDialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Incoterm[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await incotermsApi.list(currentPage + 1, pageSize);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
// Recargar automáticamente después de 2 segundos
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Page] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
// Reset y recargar desde el principio
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos después de crear/editar/eliminar
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas con el callback onSuccess
|
||||
const columns = createColumns(handleSuccess);
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Incoterms</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los términos internacionales de comercio (International Commercial Terms)
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nuevo Incoterm
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Incoterms</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de Incoterms
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => createDialogOpen = true}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Incoterm
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<Card.Root class="border-destructive">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-destructive">Error</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por clave..."
|
||||
bind:value={searchCode}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Incoterms</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2" size={16} />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<!-- TanStack DataTable con Infinite Scroll -->
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="rounded-md border">
|
||||
<DataTable
|
||||
columns={createColumns(handleSuccess)}
|
||||
data={data.items || []}
|
||||
pageCount={Math.ceil((data.total || 0) / (data.page_size || 50))}
|
||||
totalItems={data.total || 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={createDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Diálogo de crear/editar -->
|
||||
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />
|
||||
|
||||
Reference in New Issue
Block a user