Desarrollo del sistema de crud, ademas de homogenizar el estilo de las tablas

This commit is contained in:
2025-12-23 17:55:12 -06:00
parent f915bccd5f
commit 18b0a49322
91 changed files with 4745 additions and 1843 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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"""

View File

@@ -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:

View File

@@ -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(

View File

@@ -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
View 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
View 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
View 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()

View File

@@ -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

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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()}`);
}

View File

@@ -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}`);
}

View File

@@ -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()}`);
}

View File

@@ -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}`);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
})
}
];
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
})
}
];
}

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
})
}
];
}

View File

@@ -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
})
}
];
}

View File

@@ -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}
/>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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 } };

View File

@@ -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>

View File

@@ -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 } };

View File

@@ -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>

View File

@@ -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 } };

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 } };
}
};

View File

@@ -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>

View File

@@ -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 } };
}
};

View File

@@ -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>

View File

@@ -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}

View File

@@ -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 }
};
}
};

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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} />