Desarrollo de rutina de CRUD para los catalogos generales

This commit is contained in:
2025-12-22 17:43:25 -06:00
parent 6a9f69e39e
commit f915bccd5f
95 changed files with 4414 additions and 1137 deletions

View File

@@ -1,10 +1,15 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
import logging
from .models import ClassificationConcept
from .dto import ClassificationConceptCreate, ClassificationConceptUpdate
logger = logging.getLogger(__name__)
class ClassificationConceptService:
@staticmethod
@@ -76,6 +81,14 @@ class ClassificationConceptService:
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
try:
db.delete(db_obj)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"Error de integridad al eliminar clasificación de concepto {id}: {str(e)}")
raise HTTPException(
status_code=400,
detail="No se puede eliminar esta clasificación porque tiene registros relacionados (pedimentos, facturas, etc.). Primero debe eliminar o reasignar esos registros."
)

View File

@@ -40,6 +40,55 @@ async def create_company(
return service.create_company_manually(data, tenant_id=tenant_id)
@router.get(
"", # GET /api/v1/a76/company with pagination
response_model=dict,
summary="Get companies with pagination",
)
async def list_companies(
page: int = 1,
page_size: int = 50,
name: Optional[str] = None,
rfc: Optional[str] = None,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get paginated list of companies for current tenant with optional filters"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
skip = (page - 1) * page_size
filters = {}
if name:
filters["name"] = name
if rfc:
filters["rfc"] = rfc
service = CompanyService(db)
items, total = service.get_all(
db,
tenant_id,
company_id=0, # Not used for companies
skip=skip,
limit=page_size,
filters=filters if filters else None
)
total_pages = (total + page_size - 1) // page_size
return {
"items": [CompanyResponseDTO.model_validate(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
"pages": total_pages,
}
@router.get(
"/my-companies",
response_model=List[CompanyResponseDTO],
@@ -189,16 +238,89 @@ async def get_program_info(
"prosec_authorization": company.prosec_authorization,
}
base_router = TenantCRUDRoutes(
service=CompanyService,
create_schema=None,
update_schema=CompanyUpdateDTO,
response_schema=CompanyResponseDTO,
prefix="",
tags=[],
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(base_router)
@router.get(
"/{company_id}",
response_model=CompanyResponseDTO,
summary="Get company by ID",
)
async def get_company(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get a specific company by ID"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return CompanyResponseDTO.model_validate(company)
@router.put(
"/{company_id}",
response_model=CompanyResponseDTO,
summary="Update company",
)
async def update_company(
company_id: int,
data: CompanyUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Update a company"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
updated_company = CompanyService.update(
db, company_id, tenant_id, 0, data
)
if not updated_company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return CompanyResponseDTO.model_validate(updated_company)
@router.delete(
"/{company_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete company",
)
async def delete_company(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Delete a company"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
success = CompanyService.delete(db, company_id, tenant_id, 0)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return None

View File

@@ -137,10 +137,20 @@ class CompanyService:
db.delete(company)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting company {company_id}: {str(e)}")
# Check if it's a foreign key constraint
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar la empresa porque tiene registros relacionados (facturas, conceptos, etc.)"
)
raise HTTPException(status_code=400, detail="Error al eliminar la empresa")
except Exception as e:
db.rollback()
logger.error(f"Error deleting company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting company")
raise HTTPException(status_code=500, detail="Error al eliminar la empresa")
# Custom methods
def get_companies_by_tenant(self, tenant_id: int) -> List[Company]:

View File

@@ -1,10 +1,16 @@
from typing import List, Optional, Tuple, Dict, Any
import logging
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from .models import Concept
from .dto import ConceptCreate, ConceptUpdate
logger = logging.getLogger(__name__)
class ConceptService:
@staticmethod
@@ -74,6 +80,20 @@ class ConceptService:
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
try:
db.delete(db_obj)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting concept {id}: {str(e)}")
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar el concepto porque tiene registros relacionados"
)
raise HTTPException(status_code=400, detail="Error al eliminar el concepto")
except Exception as e:
db.rollback()
logger.error(f"Error deleting concept {id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error al eliminar el concepto")

View File

@@ -1,10 +1,16 @@
from typing import List, Optional, Tuple, Dict, Any
import logging
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from .models import CustomsBrokerConcept
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptUpdate
logger = logging.getLogger(__name__)
class CustomsBrokerConceptService:
@staticmethod
@@ -76,6 +82,20 @@ class CustomsBrokerConceptService:
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
try:
db.delete(db_obj)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting customs broker concept {id}: {str(e)}")
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar el concepto porque tiene registros relacionados"
)
raise HTTPException(status_code=400, detail="Error al eliminar el concepto")
except Exception as e:
db.rollback()
logger.error(f"Error deleting customs broker concept {id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error al eliminar el concepto")

View File

@@ -131,14 +131,21 @@ class DodaService:
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a DODA"""
try:
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
if not db_doda:
return False
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
if not db_doda:
return False
try:
db.delete(db_doda)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"Error de integridad al eliminar DODA {id}: {str(e)}")
raise HTTPException(
status_code=400,
detail="No se puede eliminar este DODA porque tiene registros relacionados. Primero debe eliminar o reasignar esos registros."
)
except Exception as e:
db.rollback()
logger.error(f"Error deleting DODA: {str(e)}")

View File

@@ -4,17 +4,13 @@ from .models import Legend
from .dto import LegendCreate, LegendResponse, LegendUpdate
from .service import LegendService
router = APIRouter(prefix="/legends", tags=["a76.general_catalogs.legends"])
legend_crud = TenantCRUDRoutes(
router = TenantCRUDRoutes(
service=LegendService,
create_schema=LegendCreate,
update_schema=LegendUpdate,
response_schema=LegendResponse,
prefix="/legends",
tags=["Legends"],
tags=["a76.general_catalogs.legends"],
resource_name="Legend",
enable_list=True,
)
router.include_router(legend_crud.router)
).router

View File

@@ -1,10 +1,15 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
import logging
from .models import Legend
from .dto import LegendCreate, LegendUpdate
logger = logging.getLogger(__name__)
class LegendService:
@staticmethod
@@ -63,8 +68,8 @@ class LegendService:
def update(
db: Session,
id: int,
data: LegendUpdate,
tenant_id: int,
data: LegendUpdate,
company_id: int
) -> Optional[Legend]:
db_obj = LegendService.get_by_id(db, id, tenant_id, company_id)
@@ -90,6 +95,14 @@ class LegendService:
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
try:
db.delete(db_obj)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"Error de integridad al eliminar leyenda {id}: {str(e)}")
raise HTTPException(
status_code=400,
detail="No se puede eliminar esta leyenda porque tiene registros relacionados (pedimentos, facturas, etc.). Primero debe eliminar o reasignar esos registros."
)

View File

@@ -3,11 +3,16 @@ Service layer for Packages (GBultos).
"""
from typing import Optional, Tuple, List, Dict, Any
import logging
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from . import dto, models
logger = logging.getLogger(__name__)
class PackageService:
"""Service for Package CRUD operations with tenant support"""
@@ -109,6 +114,20 @@ class PackageService:
if not package:
return False
db.delete(package)
db.commit()
return True
try:
db.delete(package)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting package {package_id}: {str(e)}")
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar el bulto porque tiene registros relacionados"
)
raise HTTPException(status_code=400, detail="Error al eliminar el bulto")
except Exception as e:
db.rollback()
logger.error(f"Error deleting package {package_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error al eliminar el bulto")

View File

@@ -1,6 +1,11 @@
from typing import List, Optional, Tuple, Dict, Any, Type
from sqlalchemy import Sequence
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
import logging
logger = logging.getLogger(__name__)
from .models import (
UnitOfMeasureACE, UnitOfMeasureOMA, UnitOfMeasureAmerican, UnitOfMeasureCustoms,
@@ -102,9 +107,17 @@ class BaseService:
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
try:
db.delete(db_obj)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"Error de integridad al eliminar {cls.model.__name__} {id}: {str(e)}")
raise HTTPException(
status_code=400,
detail="No se puede eliminar esta unidad de medida porque tiene registros relacionados. Primero debe eliminar o reasignar esos registros."
)
class UnitOfMeasureACEService(BaseService):

View File

@@ -41,11 +41,11 @@
"valuation_methods": "Valuation Methods",
"countries": "Countries",
"ports": "Ports",
"unit_measures": "Unit Measures",
"um_customs_mex": "U.M. Customs Mexico",
"um_customs_ame": "U.M. Customs America",
"um_ace": "U.M. ACE",
"um_oma": "U.M. OMA",
"unit_measures": "Units of Measure - General",
"um_customs_mex": "Units of Measure - Mexican Customs",
"um_customs_ame": "Units of Measure - American Customs",
"um_ace": "Units of Measure - ACE",
"um_oma": "Units of Measure - OMA",
"conversions": "Conversions",
"equivalences": "Equivalences",
"exchange_rates": "Exchange Rates",

View File

@@ -41,11 +41,11 @@
"valuation_methods": "Metódos de valoración",
"countries": "Países",
"ports": "Puertos",
"unit_measures": "Unidades de medida",
"um_customs_mex": "U.M. Aduanas Mexicanas",
"um_customs_ame": "U.M. Aduanas Americanas",
"um_ace": "U.M. ACE",
"um_oma": "U.M. OMA",
"unit_measures": "Unidades de medida general",
"um_customs_mex": "Unidades de medida - Aduanas Mexicanas",
"um_customs_ame": "Unidades de medida - Aduanas Americanas",
"um_ace": "Unidades de medida - ACE",
"um_oma": "Unidades de medida - OMA",
"conversions": "Conversiones",
"equivalences": "Equivalencias",
"exchange_rates": "Tipos de cambio",

View File

@@ -194,6 +194,14 @@ async function fetchApi<T = any>(
}
}
// Manejar respuestas sin contenido (204 No Content)
if (response.status === 204) {
return {
data: null as T,
status: response.status
};
}
const data = await response.json();
if (!response.ok) {

View File

@@ -37,36 +37,28 @@ export async function getClassificationConcepts(
...filters
});
const response = await api.get(`/v1/a76/classification-concepts/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/classification-concepts/?${params.toString()}`);
}
export async function getClassificationConcept(id: number, companyId: number): Promise<ClassificationConcept> {
const response = await api.get(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
return response.data;
export async function getClassificationConcept(id: number, companyId: number): Promise<ApiResponse<ClassificationConcept>> {
return await api.get(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
}
export async function createClassificationConcept(
data: ClassificationConceptCreate,
companyId: number
): Promise<ClassificationConcept> {
// 👇 AQUÍ ESTABA EL ERROR GRAVE
const response = await api.post(`/v1/a76/classification-concepts/?company_id=${companyId}`, data);
return response.data;
): Promise<ApiResponse<ClassificationConcept>> {
return await api.post(`/v1/a76/classification-concepts/?company_id=${companyId}`, data);
}
export async function updateClassificationConcept(
id: number,
data: ClassificationConceptUpdate,
companyId: number // <-- Faltaba esto
): Promise<ClassificationConcept> {
const response = await api.put(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`, data);
return response.data;
companyId: number
): Promise<ApiResponse<ClassificationConcept>> {
return await api.put(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`, data);
}
export async function deleteClassificationConcept(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
export async function deleteClassificationConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
}

View File

@@ -1,91 +1,115 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Company {
id: number;
tenant_id: number;
name: string | null;
rfc: string | null;
main_activity: string | null;
program: string | null;
program_number: string | null;
prosec: number | null;
prosec_authorization: string | null;
manufacturer_id: string | null;
broker_company: string | null;
responsible: string | null;
responsible_name: string | null;
responsible_last_name: string | null;
responsible_mother_last_name: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface Company {
id: number;
tenant_id: number;
name: string | null;
rfc: string | null;
curp?: string | null;
main_activity: string | null;
program: string | null;
program_number: string | null;
prosec: number | null;
prosec_authorization: string | null;
manufacturer_id: string | null;
broker_company: string | null;
responsible: string | null;
responsible_name: string | null;
responsible_last_name: string | null;
responsible_mother_last_name: string | null;
responsible_rfc?: string | null;
position?: string | null;
has_express_line?: boolean;
is_service_company?: boolean;
order_format_type?: string | null;
ctpat_svi?: string | null;
trusted_exporter_number?: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface CompanyCreate {
name?: string | null;
rfc?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
}
export interface CompanyCreate {
name?: string | null;
rfc?: string | null;
curp?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
responsible_rfc?: string | null;
position?: string | null;
has_express_line?: boolean;
is_service_company?: boolean;
order_format_type?: string | null;
ctpat_svi?: string | null;
trusted_exporter_number?: string | null;
}
export interface CompanyUpdate {
name?: string | null;
rfc?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
}
export interface CompanyUpdate {
name?: string | null;
rfc?: string | null;
curp?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
responsible_rfc?: string | null;
position?: string | null;
has_express_line?: boolean;
is_service_company?: boolean;
order_format_type?: string | null;
ctpat_svi?: string | null;
trusted_exporter_number?: string | null;
}
export interface CompanyListResponse {
items: Company[];
total: number;
page: number;
page_size: number;
pages: number;
}
export interface CompanyListResponse {
items: Company[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCompanies(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<CompanyListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/company?${queryParams.toString()}`);
}
export async function getCompanies(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<CompanyListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/v1/a76/company?${queryParams.toString()}`);
}
export async function getCompany(id: number): Promise<ApiResponse<Company>> {
return await api.get(`/a76/company/${id}`);
}
export async function getCompany(id: number): Promise<ApiResponse<Company>> {
return await api.get(`/v1/a76/company/${id}`);
}
export async function createCompany(data: CompanyCreate): Promise<ApiResponse<Company>> {
return await api.post(`/v1/a76/company`, data);
}
export async function createCompany(data: CompanyCreate): Promise<ApiResponse<Company>> {
return await api.post(`/v1/a76/company`, data);
}
export async function updateCompany(id: number, data: CompanyUpdate): Promise<ApiResponse<Company>> {
return await api.put(`/a76/company/${id}`, data);
}
export async function updateCompany(id: number, data: CompanyUpdate): Promise<ApiResponse<Company>> {
return await api.put(`/v1/a76/company/${id}`, data);
}
export async function deleteCompany(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/company/${id}`);
}
export async function deleteCompany(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/company/${id}`);
}

View File

@@ -57,33 +57,25 @@ export async function getConcepts(
...filters
});
const response = await api.get(`/v1/a76/concepts/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/concepts?${params.toString()}`);
}
export async function getConcept(id: number, companyId: number): Promise<Concept> {
const response = await api.get(`/v1/a76/concepts/${id}/?company_id=${companyId}`);
return response.data;
export async function getConcept(id: number, companyId: number): Promise<ApiResponse<Concept>> {
return await api.get(`/v1/a76/concepts/${id}?company_id=${companyId}`);
}
export async function createConcept(data: ConceptCreate, companyId: number): Promise<Concept> {
const response = await api.post(`/v1/a76/concepts/?company_id=${companyId}`, data);
return response.data;
export async function createConcept(data: ConceptCreate, companyId: number): Promise<ApiResponse<Concept>> {
return await api.post(`/v1/a76/concepts?company_id=${companyId}`, data);
}
export async function updateConcept(id: number, data: ConceptUpdate, companyId: number): Promise<Concept> {
const response = await api.put(`/v1/a76/concepts/${id}/?company_id=${companyId}`, data);
return response.data;
export async function updateConcept(id: number, data: ConceptUpdate, companyId: number): Promise<ApiResponse<Concept>> {
return await api.put(`/v1/a76/concepts/${id}?company_id=${companyId}`, data);
}
export async function deleteConcept(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/a76/concepts/${id}/?company_id=${companyId}`);
export async function deleteConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/concepts/${id}?company_id=${companyId}`);
}

View File

@@ -0,0 +1,75 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface CustomsBrokerConcept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface CustomsBrokerConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
}
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
export interface CustomsBrokerConceptListResponse {
items: CustomsBrokerConcept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCustomsBrokerConcepts(
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {},
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
}
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
}
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, data);
}
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
}
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
}

View File

@@ -38,35 +38,28 @@ export async function getLegends(
company_id: companyId.toString(),
...filters
});
const response = await api.get(`/v1/a76/legends/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/legends/?${params.toString()}`);
}
export async function getLegend(id: number, companyId: number): Promise<Legend> {
const response = await api.get(`/v1/a76/legends/${id}/?company_id=${companyId}`);
return response.data;
export async function getLegend(id: number, companyId: number): Promise<ApiResponse<Legend>> {
return await api.get(`/v1/a76/legends/${id}/?company_id=${companyId}`);
}
export async function createLegend(
data: LegendCreate,
companyId: number
): Promise<Legend> {
const response = await api.post(`/v1/a76/legends/?company_id=${companyId}`, data);
return response.data;
): Promise<ApiResponse<Legend>> {
return await api.post(`/v1/a76/legends/?company_id=${companyId}`, data);
}
export async function updateLegend(
id: number,
data: LegendUpdate,
companyId: number
): Promise<Legend> {
const response = await api.put(`/v1/a76/legends/${id}/?company_id=${companyId}`, data);
return response.data;
): Promise<ApiResponse<Legend>> {
return await api.put(`/v1/a76/legends/${id}/?company_id=${companyId}`, data);
}
export async function deleteLegend(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/a76/legends/${id}/?company_id=${companyId}`);
export async function deleteLegend(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/legends/${id}/?company_id=${companyId}`);
}

View File

@@ -1,79 +1,78 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
import type { PaginatedResponse } from '$lib/types';
export interface MultiCurrencyType {
id: number;
currency_type_code: string;
country_key: string | null;
conversion_factor: number | null;
publication_date: number;
tenant_id: number;
company_id: number;
created_at: string;
updated_at?: string;
id: number;
currency_type_code: string;
country_key: string | null;
conversion_factor: number | null;
publication_date: number;
company_id: number;
tenant_id: number;
}
export interface MultiCurrencyTypeCreate {
currency_type_code: string;
country_key?: string | null;
conversion_factor?: number | null;
publication_date: number;
currency_type_code: string;
country_key?: string | null;
conversion_factor?: number | null;
publication_date: number;
}
export interface MultiCurrencyTypeUpdate extends Partial<MultiCurrencyTypeCreate> {}
export interface MultiCurrencyTypeUpdate {
currency_type_code?: string;
country_key?: string | null;
conversion_factor?: number | null;
publication_date?: number;
}
export interface MultiCurrencyTypeListResponse {
items: MultiCurrencyType[];
total: number;
page: number;
page_size: number;
pages: number;
export interface MultiCurrencyTypeListResponse extends PaginatedResponse {
items: MultiCurrencyType[];
}
export async function getMultiCurrencyTypes(
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<MultiCurrencyTypeListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
companyId: number,
page?: number,
pageSize?: number
): Promise<MultiCurrencyTypeListResponse> {
const params = new URLSearchParams({ company_id: companyId.toString() });
if (page) params.append('page', page.toString());
if (pageSize) params.append('page_size', pageSize.toString());
// Agregamos /v1 y el path correcto
const response = await api.get(`/v1/a76/multi_currency_types/?${params.toString()}`);
return response.data;
return api.get<MultiCurrencyTypeListResponse>(`/v1/a76/multi-currency-types/?${params.toString()}`);
}
export async function getMultiCurrencyType(id: number, companyId: number): Promise<MultiCurrencyType> {
const response = await api.get(`/v1/a76/multi_currency_types/${id}/?company_id=${companyId}`);
return response.data;
export async function getMultiCurrencyType(
multiCurrencyTypeId: number,
companyId: number
): Promise<MultiCurrencyType> {
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.get<MultiCurrencyType>(`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`);
}
export async function createMultiCurrencyType(
data: MultiCurrencyTypeCreate,
companyId: number
data: MultiCurrencyTypeCreate,
companyId: number
): Promise<MultiCurrencyType> {
const response = await api.post(`/v1/a76/multi_currency_types/?company_id=${companyId}`, data);
return response.data;
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.post<MultiCurrencyType>(`/v1/a76/multi-currency-types/?${params.toString()}`, data);
}
export async function updateMultiCurrencyType(
id: number,
data: MultiCurrencyTypeUpdate,
companyId: number
multiCurrencyTypeId: number,
data: MultiCurrencyTypeUpdate,
companyId: number
): Promise<MultiCurrencyType> {
const response = await api.put(`/v1/a76/multi_currency_types/${id}/?company_id=${companyId}`, data);
return response.data;
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.put<MultiCurrencyType>(
`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`,
data
);
}
export async function deleteMultiCurrencyType(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/a76/multi_currency_types/${id}/?company_id=${companyId}`);
export async function deleteMultiCurrencyType(
multiCurrencyTypeId: number,
companyId: number
): Promise<void> {
const params = new URLSearchParams({ company_id: companyId.toString() });
return api.delete(`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`);
}

View File

@@ -52,22 +52,19 @@ export async function getPackages(
...filters
});
const response = await api.get(`/v1/a76/packages/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/packages?${params.toString()}`);
}
export async function getPackage(id: number, companyId: number): Promise<Package> {
const response = await api.get(`/v1/a76/packages/${id}/?company_id=${companyId}`);
return response.data;
export async function getPackage(id: number, companyId: number): Promise<ApiResponse<Package>> {
return await api.get(`/v1/a76/packages/${id}?company_id=${companyId}`);
}
export async function createPackage(
data: PackageCreate,
companyId: number
): Promise<Package> {
const response = await api.post(`/v1/a76/packages/?company_id=${companyId}`, data);
return response.data;
): Promise<ApiResponse<Package>> {
return await api.post(`/v1/a76/packages?company_id=${companyId}`, data);
}
@@ -75,11 +72,10 @@ export async function updatePackage(
id: number,
data: PackageUpdate,
companyId: number
): Promise<Package> {
const response = await api.put(`/v1/a76/packages/${id}/?company_id=${companyId}`, data);
return response.data;
): Promise<ApiResponse<Package>> {
return await api.put(`/v1/a76/packages/${id}?company_id=${companyId}`, data);
}
export async function deletePackage(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/a76/packages/${id}/?company_id=${companyId}`);
export async function deletePackage(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/packages/${id}?company_id=${companyId}`);
}

View File

@@ -73,5 +73,5 @@ export async function updateUnitConversion(
}
export async function deleteUnitConversion(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/a76/unit_conversions/${id}/?company_id=${companyId}`);
await api.delete(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
}

View File

@@ -31,26 +31,28 @@ export interface UnitOfMeasureACEListResponse {
export async function getUnitsOfMeasureACE(
page = 1,
pageSize = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureACEListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/ace?${queryParams.toString()}`);
return await api.get(`/v1/a76/units-of-measure/ace/?${queryParams.toString()}`);
}
export async function createUnitOfMeasureACE(data: UnitOfMeasureACECreate): Promise<ApiResponse<UnitOfMeasureACE>> {
return await api.post('/a76/units-of-measure/ace', data);
export async function createUnitOfMeasureACE(data: UnitOfMeasureACECreate, companyId: number): Promise<ApiResponse<UnitOfMeasureACE>> {
return await api.post(`/v1/a76/units-of-measure/ace/?company_id=${companyId}`, data);
}
export async function updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate): Promise<ApiResponse<UnitOfMeasureACE>> {
return await api.put(`/a76/units-of-measure/ace/${id}`, data);
export async function updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureACE>> {
return await api.put(`/v1/a76/units-of-measure/ace/${id}/?company_id=${companyId}`, data);
}
export async function deleteUnitOfMeasureACE(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/ace/${id}`);
export async function deleteUnitOfMeasureACE(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/units-of-measure/ace/${id}/?company_id=${companyId}`);
}
// --- OMA ---
@@ -81,28 +83,30 @@ export interface UnitOfMeasureOMAListResponse {
}
export async function getUnitsOfMeasureOMA(
page = 1,
pageSize = 50,
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureOMAListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/oma?${queryParams.toString()}`);
return await api.get(`/v1/a76/units-of-measure/oma/?${queryParams.toString()}`);
}
export async function createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate): Promise<ApiResponse<UnitOfMeasureOMA>> {
return await api.post('/a76/units-of-measure/oma', data);
export async function createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate, companyId: number): Promise<ApiResponse<UnitOfMeasureOMA>> {
return await api.post(`/v1/a76/units-of-measure/oma/?company_id=${companyId}`, data);
}
export async function updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate): Promise<ApiResponse<UnitOfMeasureOMA>> {
return await api.put(`/a76/units-of-measure/oma/${id}`, data);
export async function updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureOMA>> {
return await api.put(`/v1/a76/units-of-measure/oma/${id}/?company_id=${companyId}`, data);
}
export async function deleteUnitOfMeasureOMA(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/oma/${id}`);
export async function deleteUnitOfMeasureOMA(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/units-of-measure/oma/${id}/?company_id=${companyId}`);
}
// --- American ---
@@ -133,26 +137,139 @@ export interface UnitOfMeasureAmericanListResponse {
}
export async function getUnitsOfMeasureAmerican(
page = 1,
pageSize = 50,
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureAmericanListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/american?${queryParams.toString()}`);
return await api.get(`/v1/a76/units-of-measure/american/?${queryParams.toString()}`);
}
export async function createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate): Promise<ApiResponse<UnitOfMeasureAmerican>> {
return await api.post('/a76/units-of-measure/american', data);
export async function createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureAmerican>> {
return await api.post(`/v1/a76/units-of-measure/american/?company_id=${companyId}`, data);
}
export async function updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate): Promise<ApiResponse<UnitOfMeasureAmerican>> {
return await api.put(`/a76/units-of-measure/american/${id}`, data);
export async function updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureAmerican>> {
return await api.put(`/v1/a76/units-of-measure/american/${id}/?company_id=${companyId}`, data);
}
export async function deleteUnitOfMeasureAmerican(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/american/${id}`);
export async function deleteUnitOfMeasureAmerican(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/units-of-measure/american/${id}/?company_id=${companyId}`);
}
// --- General ---
export interface UnitOfMeasureGeneral {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UnitOfMeasureGeneralCreate {
code: string;
description?: string | null;
}
export interface UnitOfMeasureGeneralUpdate {
code?: string;
description?: string | null;
}
export interface UnitOfMeasureGeneralListResponse {
items: UnitOfMeasureGeneral[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getUnitsOfMeasureGeneral(
page = 1,
pageSize = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureGeneralListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/v1/a76/units-of-measure/general/?${queryParams.toString()}`);
}
export async function createUnitOfMeasureGeneral(data: UnitOfMeasureGeneralCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureGeneral>> {
return await api.post(`/v1/a76/units-of-measure/general/?company_id=${companyId}`, data);
}
export async function updateUnitOfMeasureGeneral(id: number, data: UnitOfMeasureGeneralUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureGeneral>> {
return await api.put(`/v1/a76/units-of-measure/general/${id}/?company_id=${companyId}`, data);
}
export async function deleteUnitOfMeasureGeneral(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/units-of-measure/general/${id}/?company_id=${companyId}`);
}
// --- Customs ---
export interface UnitOfMeasureCustoms {
id: number;
code: string;
description: string | null;
scaii_unit_code: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UnitOfMeasureCustomsCreate {
code: string;
description?: string | null;
scaii_unit_code?: string | null;
}
export interface UnitOfMeasureCustomsUpdate {
code?: string;
description?: string | null;
scaii_unit_code?: string | null;
}
export interface UnitOfMeasureCustomsListResponse {
items: UnitOfMeasureCustoms[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getUnitsOfMeasureCustoms(
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureCustomsListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/v1/a76/units-of-measure/customs/?${queryParams.toString()}`);
}
export async function createUnitOfMeasureCustoms(data: UnitOfMeasureCustomsCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureCustoms>> {
return await api.post(`/v1/a76/units-of-measure/customs/?company_id=${companyId}`, data);
}
export async function updateUnitOfMeasureCustoms(id: number, data: UnitOfMeasureCustomsUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureCustoms>> {
return await api.put(`/v1/a76/units-of-measure/customs/${id}/?company_id=${companyId}`, data);
}
export async function deleteUnitOfMeasureCustoms(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/units-of-measure/customs/${id}/?company_id=${companyId}`);
}

View File

@@ -5,44 +5,44 @@ import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<ExchangeRate>[] {
return [
{
accessorKey: 'date',
header: 'Fecha',
cell: ({ row }) => {
const dateStr = row.original.date;
if (!dateStr) return 'N/A';
const date = new Date(dateStr);
return date.toLocaleDateString('es-MX');
{
accessorKey: 'date',
header: 'Fecha',
cell: ({ row }) => {
const dateStr = row.original.date;
if (!dateStr) return 'N/A';
const date = new Date(dateStr);
return date.toLocaleDateString('es-MX');
}
},
{
accessorKey: 'value',
header: 'Valor',
cell: ({ row }) => {
const value = row.original.value;
if (value === null || value === undefined) return 'N/A';
return value.toFixed(6);
}
},
{
accessorKey: 'local_currency',
header: 'Moneda Local',
cell: ({ row }) => row.original.local_currency ?? 'N/A'
},
{
accessorKey: 'foreign_currency',
header: 'Moneda Extranjera',
cell: ({ row }) => row.original.foreign_currency ?? 'N/A'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
},
{
accessorKey: 'value',
header: 'Tipo de Cambio',
cell: ({ row }) => {
const value = row.original.value;
if (value === null || value === undefined) return 'N/A';
return value.toFixed(6);
}
},
{
accessorKey: 'local_currency',
header: 'Moneda Local',
cell: ({ row }) => row.original.local_currency ?? 'N/A'
},
{
accessorKey: 'foreign_currency',
header: 'Moneda Extranjera',
cell: ({ row }) => row.original.foreign_currency ?? 'N/A'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -75,8 +75,10 @@
if (isEdit && item) {
await updateExchangeRate(item.id, dataToSend, companyId);
alert(`✅ Tipo de cambio actualizado correctamente`);
} else {
await createExchangeRate(dataToSend, companyId);
alert(`✅ Tipo de cambio creado correctamente`);
}
open = false;

View File

@@ -27,23 +27,21 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('No hay compañía seleccionada');
alert('❌ Error: No hay compañía seleccionada');
return;
}
loading = true;
error = null;
try {
await deleteExchangeRate(item.id, companyId);
// Éxito
alert(`✅ Tipo de cambio del ${new Date(item.date).toLocaleDateString('es-MX')} eliminado correctamente`);
if (onSuccess) {
onSuccess();
}
} catch (err: any) {
error = err.message || 'Error al eliminar el tipo de cambio';
alert(`Error: ${error}`);
alert(`Error: ${error}`);
console.error('Error deleting:', err);
} finally {
loading = false;

View File

@@ -1,123 +1,106 @@
<script lang="ts" generics="TData, TValue">
import { onMount } from 'svelte';
import {
type ColumnDef,
getCoreRowModel
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
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[];
loading: boolean;
hasMore: boolean;
loadMore: () => void;
};
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
loading,
hasMore,
loadMore
}: DataTableProps<TData, TValue> = $props();
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel()
});
const table = createSvelteTable({
get data() { return data; },
get columns() { return columns; },
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
get pageCount() { return pageCount; },
});
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url, { keepFocus: true, noScroll: true });
}
// Intersection Observer para detectar cuando el usuario llega al final
onMount(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
},
{
root: scrollContainer,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
return () => {
observer.disconnect();
};
});
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
</script>
<div class="w-full">
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
{#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}
<!-- Loading Trigger - Se activa cuando es visible -->
{#if hasMore}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-20 text-center">
<div bind:this={loadingTrigger}>
{#if loading}
<div class="flex items-center justify-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="text-muted-foreground text-sm">Cargando más...</span>
</div>
{:else}
<div class="text-muted-foreground text-sm">
Desplázate para cargar más
</div>
{/if}
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
<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,29 @@
import type { ClassificationConcept } from '$lib/api/dashboard/a76/general_catalogs/classification-concepts';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<ClassificationConcept>[] {
return [
{
accessorKey: 'classification',
header: 'Clasificación',
cell: ({ row }) => row.original.classification || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,89 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteClassificationConcept, type ClassificationConcept } from "$lib/api/dashboard/a76/general_catalogs/classification-concepts";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "$lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: ClassificationConcept;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la clasificación "${item.classification}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await deleteClassificationConcept(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Clasificación "${item.classification}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
}
</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={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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={item}
onSuccess={onSuccess}
/>

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

@@ -18,7 +18,7 @@
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la empresa "${item.name}"?`)) {
if (!confirm(`¿Estás seguro de eliminar la empresa "${item.name}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
@@ -28,16 +28,23 @@
try {
const response = await deleteCompany(item.id);
// Si hay error en la respuesta
if (response.error) {
alert(`Error al eliminar: ${response.error}`);
alert(`Error al eliminar:\n\n${response.error}`);
return;
}
if (onSuccess) {
onSuccess();
// Éxito (status 204 o 200)
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Empresa "${item.name}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
alert('Error al eliminar el registro');
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
@@ -55,7 +62,7 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>

View File

@@ -0,0 +1,109 @@
<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";
// Props exactos que manda tu página de Companies
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; },
});
// Función para navegar cambiando la URL ?page=X
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url, { keepFocus: true, noScroll: true }); // Truco: noScroll evita saltos feos
}
// Helper para saber la página actual
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,39 @@
import type { Concept } from '$lib/api/dashboard/a76/general_catalogs/concepts';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Concept>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'type',
header: 'Tipo',
cell: ({ row }) => row.original.type || '-'
},
{
accessorKey: 'section',
header: 'Sección',
cell: ({ row }) => row.original.section?.toString() || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,89 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteConcept, type Concept } from "$lib/api/dashboard/a76/general_catalogs/concepts";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Concept;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el concepto "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await deleteConcept(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
}
</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={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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={item}
onSuccess={onSuccess}
/>

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,39 @@
import type { CustomsBrokerConcept } from '$lib/api/dashboard/a76/general_catalogs/customs_broker_concepts';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBrokerConcept>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'type',
header: 'Tipo',
cell: ({ row }) => row.original.type || '-'
},
{
accessorKey: 'section',
header: 'Sección',
cell: ({ row }) => row.original.section?.toString() || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,89 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteCustomsBrokerConcept, type CustomsBrokerConcept } from "$lib/api/dashboard/a76/general_catalogs/customs_broker_concepts";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edite-dialoge.svelte";
let {
item,
onSuccess
}: {
item: CustomsBrokerConcept;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el concepto "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await deleteCustomsBrokerConcept(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
}
</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={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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={item}
onSuccess={onSuccess}
/>

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

@@ -71,13 +71,17 @@
let response;
if (isEdit && item) {
response = await updateLegend(item.id, dataToSend, companyId);
} else {
response = await createLegend(dataToSend, companyId);
}
// Verificar si hubo error
if (response.error) {
throw new Error(response.error);
}
open = false;
if (onSuccess) onSuccess();

View File

@@ -0,0 +1,29 @@
import type { Legend } from '$lib/api/dashboard/a76/general_catalogs/legends';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Legend>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code?.toString() || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

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 { deleteLegend, type Legend } from "$lib/api/dashboard/a76/general_catalogs/legends";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte';
let {
item,
onSuccess
}: {
item: Legend;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la leyenda "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await deleteLegend(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Leyenda "${item.code}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
}
</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={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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>
<CreateDialog
bind:open={dialogOpen}
item={item}
onSuccess={onSuccess}
/>

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

@@ -90,14 +90,13 @@
conversion_factor: formData.conversion_factor ? Number(formData.conversion_factor) : null,
publication_date: dateInt // Mandamos el INT que espera Python
};
let response;
// 👇 companyId por fuera
if (isEdit && item) {
response = await updateMultiCurrencyType(item.id, dataToSend, companyId);
await updateMultiCurrencyType(item.id, dataToSend, companyId);
alert(`✅ Tipo de moneda múltiple actualizado correctamente`);
} else {
response = await createMultiCurrencyType(dataToSend, companyId);
await createMultiCurrencyType(dataToSend, companyId);
alert(`✅ Tipo de moneda múltiple creado correctamente`);
}
open = false;

View File

@@ -0,0 +1,32 @@
import type { ColumnDef } from '@tanstack/table-core';
import type { UnitConversion } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitConversion>[] {
return [
{
accessorKey: 'from_unit_code',
header: 'Desde código'
},
{
accessorKey: 'to_unit_code',
header: 'Hacia código'
},
{
accessorKey: 'conversion_factor',
header: 'Factor de conversión'
},
{
id: 'actions',
cell: ({ row }) => {
return {
component: DataTableActions,
props: {
conversion: row.original,
onSuccess
}
};
}
}
];
}

View File

@@ -0,0 +1,148 @@
<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 { createUnitConversion, updateUnitConversion, type UnitConversion } from "$lib/api/dashboard/a76/general_catalogs/unit-conversions";
import { companyStore } from "$lib/stores/company.svelte";
let {
open = $bindable(false),
conversion = null,
mode = 'create',
onSuccess
}: {
open: boolean;
conversion?: UnitConversion | null;
mode?: 'create' | 'edit';
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Conversión" : "Nueva Conversión");
let formData = $state({
from_unit_code: '',
to_unit_code: '',
conversion_factor: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (conversion) {
formData = {
from_unit_code: conversion.from_unit_code || '',
to_unit_code: conversion.to_unit_code || '',
conversion_factor: conversion.conversion_factor.toString() || ''
};
} else {
formData = {
from_unit_code: '',
to_unit_code: '',
conversion_factor: ''
};
}
});
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.from_unit_code.trim()) throw new Error('El código origen es requerido');
if (!formData.to_unit_code.trim()) throw new Error('El código destino es requerido');
if (!formData.conversion_factor || formData.conversion_factor === '') throw new Error('El factor de conversión es requerido');
const dataToSend = {
from_unit_code: formData.from_unit_code.trim(),
to_unit_code: formData.to_unit_code.trim(),
conversion_factor: parseFloat(formData.conversion_factor)
};
if (isNaN(dataToSend.conversion_factor)) {
throw new Error('El factor de conversión debe ser un número válido');
}
if (isEdit && conversion) {
await updateUnitConversion(conversion.id, dataToSend, companyId);
alert(`✅ Conversión "${dataToSend.from_unit_code}${dataToSend.to_unit_code}" actualizada correctamente`);
} else {
await createUnitConversion(dataToSend, companyId);
alert(`✅ Conversión "${dataToSend.from_unit_code}${dataToSend.to_unit_code}" 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.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
{#if error}
<div class="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
{/if}
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="from_unit_code">Código origen <span class="text-destructive">*</span></Label>
<Input
id="from_unit_code"
bind:value={formData.from_unit_code}
maxlength={5}
disabled={loading}
required
/>
</div>
<div class="grid gap-2">
<Label for="to_unit_code">Código destino <span class="text-destructive">*</span></Label>
<Input
id="to_unit_code"
bind:value={formData.to_unit_code}
maxlength={5}
disabled={loading}
required
/>
</div>
<div class="grid gap-2">
<Label for="conversion_factor">Factor de conversión <span class="text-destructive">*</span></Label>
<Input
id="conversion_factor"
type="number"
step="0.000001"
bind:value={formData.conversion_factor}
disabled={loading}
required
/>
</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.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUnitConversion, type UnitConversion } from "$lib/api/dashboard/a76/general_catalogs/unit-conversions";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
conversion,
onSuccess
}: {
conversion: UnitConversion;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la conversión "${conversion.from_unit_code}${conversion.to_unit_code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
try {
await deleteUnitConversion(conversion.id, companyStore.activeCompany.id);
alert(`✅ Conversión "${conversion.from_unit_code}${conversion.to_unit_code}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
}
</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={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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}
conversion={conversion}
mode="edit"
{onSuccess}
/>

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,29 @@
import type { UnitOfMeasureACE } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureACE>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

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 { createUnitOfMeasureACE, updateUnitOfMeasureACE, type UnitOfMeasureACE } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
import { companyStore } from "$lib/stores/company.svelte";
let {
open = $bindable(false),
item = null,
onSuccess
}: {
open: boolean;
item?: UnitOfMeasureACE | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? "Editar Unidad ACE" : "Nueva Unidad ACE");
let formData = $state({
code: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (item) {
formData = {
code: item.code || '',
description: item.description || ''
};
} else {
formData = {
code: '',
description: ''
};
}
});
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 dataToSend = {
code: formData.code.trim(),
description: formData.description.trim() || null
};
let response;
if (isEdit && item) {
response = await updateUnitOfMeasureACE(item.id, dataToSend, companyId);
} else {
response = await createUnitOfMeasureACE(dataToSend, companyId);
}
if (response.error) {
throw new Error(response.error);
}
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.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
{#if error}
<div class="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
{/if}
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="code">Código <span class="text-destructive">*</span></Label>
<Input
id="code"
bind:value={formData.code}
maxlength={4}
disabled={loading}
required
/>
</div>
<div class="grid gap-2">
<Label for="description">Descripción</Label>
<Input
id="description"
bind:value={formData.description}
maxlength={49}
disabled={loading}
/>
</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.Root>

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUnitOfMeasureACE, type UnitOfMeasureACE } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
import { companyStore } from "$lib/stores/company.svelte";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureACE;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la unidad ACE "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
try {
const response = await deleteUnitOfMeasureACE(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Unidad ACE "${item.code}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}
}
</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={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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={item}
onSuccess={onSuccess}
/>

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,25 @@
import type { ColumnDef } from "@tanstack/table-core";
import type { UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureAmerican>[] {
return [
{
accessorKey: "code",
header: "Código",
},
{
accessorKey: "description",
header: "Descripción",
},
{
id: "actions",
cell: ({ row }) =>
renderComponent(DataTableActions, {
unit: row.original,
onSuccess
}),
},
];
}

View File

@@ -0,0 +1,91 @@
<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 type {
UnitOfMeasureAmerican,
UnitOfMeasureAmericanCreate,
UnitOfMeasureAmericanUpdate
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import {
createUnitOfMeasureAmerican,
updateUnitOfMeasureAmerican
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
open: boolean;
unit?: UnitOfMeasureAmerican;
onSuccess?: () => void;
}
let { open = $bindable(), unit, onSuccess }: Props = $props();
let formData = $state({
code: '',
description: ''
});
$effect(() => {
if (open) {
if (unit) {
formData = {
code: unit.code,
description: unit.description || ''
};
} else {
formData = { code: '', description: '' };
}
}
});
async function handleSubmit(e: Event) {
e.preventDefault();
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const data: UnitOfMeasureAmericanCreate | UnitOfMeasureAmericanUpdate = {
code: formData.code,
description: formData.description || null
};
const response = unit
? await updateUnitOfMeasureAmerican(unit.id, data, activeCompanyId)
: await createUnitOfMeasureAmerican(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{unit ? 'Editar' : 'Crear'} Unidad Americana</Dialog.Title>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-4">
<div class="space-y-2">
<Label for="code">Código * (máx. 3 caracteres)</Label>
<Input id="code" bind:value={formData.code} required maxlength="3" />
</div>
<div class="space-y-2">
<Label for="description">Descripción (máx. 40 caracteres)</Label>
<Input id="description" bind:value={formData.description} maxlength="40" />
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button type="submit">Guardar</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
import type { UnitOfMeasureAmerican } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { deleteUnitOfMeasureAmerican } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
unit: UnitOfMeasureAmerican;
onSuccess?: () => void;
}
let { unit, onSuccess }: Props = $props();
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm('¿Estás seguro de eliminar esta unidad?')) return;
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureAmerican(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}
</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>
<MoreHorizontal class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}>
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog bind:open={dialogOpen} {unit} {onSuccess} />

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,29 @@
import type { ColumnDef } from "@tanstack/table-core";
import type { UnitOfMeasureCustoms } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureCustoms>[] {
return [
{
accessorKey: "code",
header: "Código",
},
{
accessorKey: "description",
header: "Descripción",
},
{
accessorKey: "scaii_unit_code",
header: "Código SCAII",
},
{
id: "actions",
cell: ({ row }) =>
renderComponent(DataTableActions, {
unit: row.original,
onSuccess
}),
},
];
}

View File

@@ -0,0 +1,98 @@
<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 type {
UnitOfMeasureCustoms,
UnitOfMeasureCustomsCreate,
UnitOfMeasureCustomsUpdate
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import {
createUnitOfMeasureCustoms,
updateUnitOfMeasureCustoms
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
open: boolean;
unit?: UnitOfMeasureCustoms;
onSuccess?: () => void;
}
let { open = $bindable(), unit, onSuccess }: Props = $props();
let formData = $state({
code: '',
description: '',
scaii_unit_code: ''
});
$effect(() => {
if (open) {
if (unit) {
formData = {
code: unit.code,
description: unit.description || '',
scaii_unit_code: unit.scaii_unit_code || ''
};
} else {
formData = { code: '', description: '', scaii_unit_code: '' };
}
}
});
async function handleSubmit(e: Event) {
e.preventDefault();
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const data: UnitOfMeasureCustomsCreate | UnitOfMeasureCustomsUpdate = {
code: formData.code,
description: formData.description || null,
scaii_unit_code: formData.scaii_unit_code || null
};
const response = unit
? await updateUnitOfMeasureCustoms(unit.id, data, activeCompanyId)
: await createUnitOfMeasureCustoms(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{unit ? 'Editar' : 'Crear'} Unidad Aduanas MEX</Dialog.Title>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-4">
<div class="space-y-2">
<Label for="code">Código * (máx. 2 caracteres)</Label>
<Input id="code" bind:value={formData.code} required maxlength="2" />
</div>
<div class="space-y-2">
<Label for="description">Descripción (máx. 20 caracteres)</Label>
<Input id="description" bind:value={formData.description} maxlength="20" />
</div>
<div class="space-y-2">
<Label for="scaii_unit_code">Código SCAII</Label>
<Input id="scaii_unit_code" bind:value={formData.scaii_unit_code} maxlength="5" />
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button type="submit">Guardar</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
import type { UnitOfMeasureCustoms } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { deleteUnitOfMeasureCustoms } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
unit: UnitOfMeasureCustoms;
onSuccess?: () => void;
}
let { unit, onSuccess }: Props = $props();
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm('¿Estás seguro de eliminar esta unidad?')) return;
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureCustoms(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}
</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>
<MoreHorizontal class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}>
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog bind:open={dialogOpen} {unit} {onSuccess} />

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,29 @@
import type { ColumnDef } from '@tanstack/table-core';
import type { UnitOfMeasureGeneral } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureGeneral>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: (info) => info.getValue()
},
{
accessorKey: 'description',
header: 'Descripción',
cell: (info) => info.getValue() || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
unit: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,91 @@
<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 type {
UnitOfMeasureGeneral,
UnitOfMeasureGeneralCreate,
UnitOfMeasureGeneralUpdate
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import {
createUnitOfMeasureGeneral,
updateUnitOfMeasureGeneral
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
open: boolean;
unit?: UnitOfMeasureGeneral;
onSuccess?: () => void;
}
let { open = $bindable(), unit, onSuccess }: Props = $props();
let formData = $state({
code: '',
description: ''
});
$effect(() => {
if (open) {
if (unit) {
formData = {
code: unit.code,
description: unit.description || ''
};
} else {
formData = { code: '', description: '' };
}
}
});
async function handleSubmit(e: Event) {
e.preventDefault();
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const data: UnitOfMeasureGeneralCreate | UnitOfMeasureGeneralUpdate = {
code: formData.code,
description: formData.description || null
};
const response = unit
? await updateUnitOfMeasureGeneral(unit.id, data, activeCompanyId)
: await createUnitOfMeasureGeneral(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{unit ? 'Editar' : 'Crear'} Unidad General</Dialog.Title>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-4">
<div class="space-y-2">
<Label for="code">Código *</Label>
<Input id="code" bind:value={formData.code} required />
</div>
<div class="space-y-2">
<Label for="description">Descripción</Label>
<Input id="description" bind:value={formData.description} />
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button type="submit">Guardar</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
import type { UnitOfMeasureGeneral } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { deleteUnitOfMeasureGeneral } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
unit: UnitOfMeasureGeneral;
onSuccess?: () => void;
}
let { unit, onSuccess }: Props = $props();
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm('¿Estás seguro de eliminar esta unidad?')) return;
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureGeneral(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}
</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>
<MoreHorizontal class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}>
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog bind:open={dialogOpen} {unit} {onSuccess} />

View File

@@ -0,0 +1,107 @@
<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,25 @@
import type { ColumnDef } from "@tanstack/table-core";
import type { UnitOfMeasureOMA } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureOMA>[] {
return [
{
accessorKey: "code",
header: "Código",
},
{
accessorKey: "description",
header: "Descripción",
},
{
id: "actions",
cell: ({ row }) =>
renderComponent(DataTableActions, {
unit: row.original,
onSuccess
}),
},
];
}

View File

@@ -0,0 +1,91 @@
<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 type {
UnitOfMeasureOMA,
UnitOfMeasureOMACreate,
UnitOfMeasureOMAUpdate
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import {
createUnitOfMeasureOMA,
updateUnitOfMeasureOMA
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
open: boolean;
unit?: UnitOfMeasureOMA;
onSuccess?: () => void;
}
let { open = $bindable(), unit, onSuccess }: Props = $props();
let formData = $state({
code: '',
description: ''
});
$effect(() => {
if (open) {
if (unit) {
formData = {
code: unit.code,
description: unit.description || ''
};
} else {
formData = { code: '', description: '' };
}
}
});
async function handleSubmit(e: Event) {
e.preventDefault();
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const data: UnitOfMeasureOMACreate | UnitOfMeasureOMAUpdate = {
code: formData.code,
description: formData.description || null
};
const response = unit
? await updateUnitOfMeasureOMA(unit.id, data, activeCompanyId)
: await createUnitOfMeasureOMA(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{unit ? 'Editar' : 'Crear'} Unidad OMA</Dialog.Title>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-4">
<div class="space-y-2">
<Label for="code">Código * (máx. 10 caracteres)</Label>
<Input id="code" bind:value={formData.code} required maxlength="10" />
</div>
<div class="space-y-2">
<Label for="description">Descripción (máx. 200 caracteres)</Label>
<Input id="description" bind:value={formData.description} maxlength="200" />
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button type="submit">Guardar</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
import type { UnitOfMeasureOMA } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { deleteUnitOfMeasureOMA } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
import { companyStore } from '$lib/stores/company.svelte';
interface Props {
unit: UnitOfMeasureOMA;
onSuccess?: () => void;
}
let { unit, onSuccess }: Props = $props();
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm('¿Estás seguro de eliminar esta unidad?')) return;
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureOMA(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}
</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>
<MoreHorizontal class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}>
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog bind:open={dialogOpen} {unit} {onSuccess} />

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

@@ -20,7 +20,12 @@
let selectedItem = $state<Package | null>(null);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el bulto "${item.key}"?`)) {
if (!confirm(`¿Estás seguro de eliminar el bulto "${item.key}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -28,7 +33,7 @@
error = null;
try {
const response = await deletePackage(item.id);
const response = await deletePackage(item.id, companyStore.activeCompany.id);
if (response.error) {
if (response.status === 401) {
@@ -38,18 +43,21 @@
}, 1500);
} else {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
alert(`Error al eliminar:\n\n${response.error}`);
}
return;
}
// Éxito
if (onSuccess) {
onSuccess();
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Bulto "${item.key}" eliminado correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
error = e instanceof Error ? e.message : "Error al eliminar";
alert(`Error: ${error}`);
alert(`Error: ${error}`);
console.error("Error deleting:", e);
} finally {
loading = false;

View File

@@ -27,10 +27,14 @@
get data() {
return data;
},
columns,
get columns() {
return columns;
},
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: pageCount,
get pageCount() {
return pageCount;
},
});
function handlePageChange(newPage: number) {

View File

@@ -19,7 +19,26 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
if (classification) filters.classification = classification;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
// Obtener company_id de la cookie o usar el primero disponible
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No se encontró una compañía seleccionada',
classifications: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
};
}
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/classification-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/classification_concepts/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/classification_concepts/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -16,10 +17,12 @@
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'classification', label: 'Clasificación' },
{ key: 'description', label: 'Descripción' },
];
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
const columns = $derived(createColumns(handleSuccess));
function handleSearch() {
if (!browser) return;
@@ -36,11 +39,6 @@
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
@@ -74,17 +72,15 @@
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.classifications?.items || []}
columns={columns}
pageCount={data.classifications?.pages || 0}
totalItems={data.classifications?.total || 0}
/>
</div>
<DataTable
data={data.classifications?.items || []}
columns={columns}
pageCount={data.classifications?.pages || 0}
totalItems={data.classifications?.total || 0}
/>
<CreateDialog
bind:open={dialogOpen}
title="Crear Nueva Clasificación de Concepto"
on:success={handleSuccess}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -29,7 +29,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const rfc = url.searchParams.get('rfc');
if (name) filters.name = name;
if (rfc) filters.rfc = rfc;
if (rfc) filters.rfc = rfc;
// Construir URL con parámetros
const queryParams = new URLSearchParams({

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/company/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/company/columns.js';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';

View File

@@ -31,8 +31,28 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
if (code) filters.code = code;
if (description) filters.description = description;
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No se encontró una compañía seleccionada',
concepts: {
items: [],
total: 0,
page: 1,
page_size: 50,
pages: 0
}
};
}
// Construir URL con parámetros
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/concepts/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/concepts/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -16,12 +17,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' },
{ key: 'type', label: 'Tipo' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
@@ -76,9 +71,9 @@
</div>
<div class="rounded-md border">
<SimpleDataTable
<DataTable
data={data.concepts?.items || []}
columns={columns}
columns={createColumns(handleSuccess)}
pageCount={data.concepts?.pages || 0}
totalItems={data.concepts?.total || 0}
/>
@@ -86,6 +81,6 @@
<CreateEditDialog
bind:open={dialogOpen}
on:success={handleSuccess}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -19,7 +19,26 @@ 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 });
// Obtener company_id de la cookie o usar el primero disponible
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No se encontró una compañía seleccionada',
concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
};
}
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/customs-broker-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edite-dialoge.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -16,11 +17,14 @@
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' },
{ key: 'type', label: 'Tipo' },
];
// Solo necesitamos una declaración de handleSuccess
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
// columns depende de handleSuccess, así que se queda igual
const columns = $derived(createColumns(handleSuccess));
function handleSearch() {
if (!browser) return;
@@ -37,11 +41,6 @@
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
@@ -76,7 +75,7 @@
</div>
<div class="rounded-md border">
<SimpleDataTable
<DataTable
data={data.concepts?.items || []}
columns={columns}
pageCount={data.concepts?.pages || 0}
@@ -86,6 +85,6 @@
<CreateEditDialog
bind:open={dialogOpen}
on:success={handleSuccess}
onSuccess={handleSuccess}
/>
</div>
</div>

View File

@@ -2,35 +2,65 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return { error: 'No authenticated', exchangeRates: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
if (!accessToken) {
return {
error: 'No authenticated',
exchange_rates: { items: [], total: 0, page: 1, page_size: 10, pages: 0 }
};
}
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const date = url.searchParams.get('date');
const localCurrency = url.searchParams.get('local_currency');
const foreignCurrency = url.searchParams.get('foreign_currency');
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 10;
const filters: Record<string, string> = {};
if (date) filters.date = date;
if (localCurrency) filters.local_currency = localCurrency;
if (foreignCurrency) filters.foreign_currency = foreignCurrency;
// Obtener company_id
const parentData = await parent();
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/exchange-rate?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!companyId) {
return {
error: 'No company selected',
exchange_rates: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
if (!response.ok) {
return { error: 'Failed to load', exchangeRates: { 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
});
return { exchangeRates: await response.json() };
} catch (error) {
console.error('Error loading exchange rates:', error);
return { error: 'Error loading', exchangeRates: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await authenticatedFetch(
`v1/a76/exchange-rate/?${queryParams.toString()}`,
{
method: 'GET',
cache: 'no-store'
},
cookies,
fetch
);
if (!response.ok) {
return {
error: 'Failed to load',
exchange_rates: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
return { exchange_rates: await response.json() };
} catch (error) {
console.error('Error loading exchange rates:', error);
return {
error: 'Error loading',
exchange_rates: { items: [], total: 0, page: 1, page_size: 10, pages: 0 }
};
}
};

View File

@@ -1,192 +1,70 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Card from '$lib/components/ui/card';
import * as Alert from '$lib/components/ui/alert';
import { Plus, Search } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
import type { ExchangeRate } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { companyStore } from '$lib/stores/company.svelte';
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';
let { data } = $props();
let { data }: { data: PageData } = $props();
let allItems = $state<ExchangeRate[]>(data.exchangeRates?.items || []);
let loading = $state(false);
let error = $state<string | null>(data.error || null);
let currentPage = $state(1);
const pageSize = 50;
let createDialogOpen = $state(false);
let loading = $state(false);
let dialogOpen = $state(false);
let editingItem = $state<ExchangeRate | null>(null);
const columns = createColumns(() => {
refreshData();
});
// Filters
let dateFilter = $state('');
let localCurrencyFilter = $state('');
let foreignCurrencyFilter = $state('');
let hasMore = $derived(allItems.length >= currentPage * pageSize);
onMount(() => {
// Sync access_token from cookies to localStorage
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'access_token') {
localStorage.setItem('access_token', value);
break;
}
}
// Carga inicial reactiva al store
if (companyStore.activeCompany) {
loadInitialData();
}
});
// Efecto reactivo: si cambia la compañía, recargar
$effect(() => {
if (companyStore.activeCompany?.id) {
loadInitialData();
}
});
async function loadInitialData() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
allItems = [];
currentPage = 1;
await loadExchangeRates(1);
}
async function loadExchangeRates(page: number) {
const companyId = companyStore.activeCompany?.id;
if (!companyId || loading) return;
loading = true;
error = null;
try {
const filters: any = {
page,
page_size: pageSize
};
if (dateFilter) filters.date = dateFilter;
if (localCurrencyFilter) filters.local_currency = localCurrencyFilter;
if (foreignCurrencyFilter) filters.foreign_currency = foreignCurrencyFilter;
const response = await getExchangeRates(companyId, filters);
if (page === 1) {
allItems = response.items;
} else {
allItems = [...allItems, ...response.items];
}
currentPage = page;
} catch (err: any) {
error = err.message || 'Error al cargar los tipos de cambio';
} finally {
loading = false;
}
}
function loadMore() {
if (!loading && hasMore) {
loadExchangeRates(currentPage + 1);
}
}
function handleSearch() {
loadInitialData();
}
function handleCreate() {
editingItem = null;
dialogOpen = true;
}
// Esta función se pasa a las columnas
function handleEdit(item: ExchangeRate) {
editingItem = item;
dialogOpen = true;
}
function handleSuccess(item?: ExchangeRate) {
loadInitialData();
}
const columns = createColumns(handleEdit); // Pasamos handleEdit en lugar de handleSuccess
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;
}
</script>
<svelte:head>
<title>Tipos de Cambio - Anexo 76</title>
</svelte:head>
<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="space-y-6 p-4">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold tracking-tight">Tipos de Cambio</h1>
<p class="text-muted-foreground">Gestiona los tipos de cambio del sistema</p>
</div>
<Button href="/dashboard/general_catalogs/exchange-rate/new">
<Plus class="mr-2 h-4 w-4" />
Crear 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>
<Card.Root>
<Card.Header>
<Card.Title>Filtros</Card.Title>
</Card.Header>
<Card.Content>
<div class="grid gap-4 md:grid-cols-4 items-end">
<div class="space-y-2">
<Label for="date-filter">Fecha</Label>
<Input id="date-filter" type="date" bind:value={dateFilter} />
</div>
<div class="space-y-2">
<Label for="local-currency-filter">Moneda Local</Label>
<Input id="local-currency-filter" placeholder="MXN" bind:value={localCurrencyFilter} />
</div>
<div class="space-y-2">
<Label for="foreign-currency-filter">Moneda Extranjera</Label>
<Input id="foreign-currency-filter" placeholder="USD" bind:value={foreignCurrencyFilter} />
</div>
<Button onclick={handleSearch} class="w-full" variant="secondary">
<Search class="mr-2 h-4 w-4" />
Buscar
</Button>
</div>
</Card.Content>
</Card.Root>
{#if error}
<Alert.Root variant="destructive">
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
<div class="rounded-md border bg-card">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
loadMore={loadMore}
/>
</div>
<CreateEditDialog
bind:open={dialogOpen}
item={editingItem}
onSuccess={handleSuccess}
/>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -19,7 +19,26 @@ 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 });
// Obtener company_id de la cookie o usar el primero disponible
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No se encontró una compañía seleccionada',
legends: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
};
}
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/legends?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/legends/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/legends/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -16,10 +17,12 @@
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 handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
const columns = $derived(createColumns(handleSuccess));
function handleSearch() {
if (!browser) return;
@@ -36,11 +39,6 @@
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
@@ -74,17 +72,15 @@
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.legends?.items || []}
columns={columns}
pageCount={data.legends?.pages || 0}
totalItems={data.legends?.total || 0}
/>
</div>
<DataTable
data={data.legends?.items || []}
columns={columns}
pageCount={data.legends?.pages || 0}
totalItems={data.legends?.total || 0}
/>
<CreateDialog
bind:open={dialogOpen}
on:success={handleSuccess}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -12,15 +12,28 @@ 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 filters: Record<string, string> = {};
const key = url.searchParams.get('key');
const description = url.searchParams.get('description');
// Obtener company_id
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (key) filters.key = key;
if (description) filters.description = description;
if (!companyId) {
return {
error: 'No company selected',
types: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/multi-currency-types?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString()
});
const response = await authenticatedFetch(`v1/a76/multi-currency-types/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', types: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };

View File

@@ -3,40 +3,19 @@
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/multi_currency_types/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 { goto } from '$app/navigation';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchKey = $state($page.url.searchParams.get('key') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'key', label: 'Clave' },
{ key: 'description', label: 'Descripción' },
{ key: 'currency_type_code', label: 'Código Moneda' },
{ key: 'country_key', label: 'País' },
{ key: 'conversion_factor', label: 'Factor Conversión' },
{ key: 'publication_date', label: 'Fecha Publicación' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchKey) url.searchParams.set('key', searchKey);
else url.searchParams.delete('key');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
@@ -57,23 +36,6 @@
</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={searchKey}
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>
<div class="rounded-md border">
<SimpleDataTable
data={data.types?.items || []}

View File

@@ -31,8 +31,28 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
if (key) filters.key = key;
if (description_es) filters.description_es = description_es;
// Obtener company_id de la cookie o usar el primero disponible
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No se encontró una compañía seleccionada',
packages: {
items: [],
total: 0,
page: 1,
page_size: 50,
pages: 0
}
};
}
// Construir URL con parámetros
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters

View File

@@ -2,28 +2,62 @@ import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return { error: 'No authenticated', conversions: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
if (!accessToken) {
return {
error: 'No authenticated',
conversions: { 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 filters: Record<string, string> = {};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/unit-conversions?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
// Obtener company_id
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!response.ok) {
return { error: 'Failed to load', conversions: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
if (!companyId) {
return {
error: 'No company selected',
conversions: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
return { conversions: await response.json() };
} catch (error) {
console.error('Error loading unit conversions:', error);
return { error: 'Error loading', conversions: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(
`v1/a76/unit-conversions/?${queryParams.toString()}`,
{ method: 'GET' },
cookies,
fetch
);
if (!response.ok) {
return {
error: 'Failed to load',
conversions: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
return { conversions: await response.json() };
} catch (error) {
console.error('Error loading unit conversions:', error);
return {
error: 'Error loading',
conversions: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
};
}
};

View File

@@ -1,53 +1,62 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import CreateDialogEdit from '$lib/components/dashboard/general_catalogs/unit-conversion/create-edite-dialoge.svelte';
import { Button } from '$lib/components/ui/button';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/general_catalogs/unit_conversions/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/unit_conversions/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/unit_conversions/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';
let { data } = $props();
let dialogOpen = $state(false);
let { data }: { data: PageData } = $props();
const columns = [
{ key: 'from_unit_id', label: 'De Unidad' },
{ key: 'to_unit_id', label: 'A Unidad' },
{ key: 'conversion_factor', label: 'Factor de Conversión' },
];
let createDialogOpen = $state(false);
let loading = $state(false);
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<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">Conversiones de Unidades</h1>
<p class="text-muted-foreground">
Gestión del catálogo de conversiones de unidades de medida
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Conversión
</Button>
</div>
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Conversiones de Unidades</h2>
<p class="text-muted-foreground">
Catálogo de conversiones de unidades de medida
</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" />
Nueva Conversión
</Button>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.conversions?.items || []}
columns={columns}
pageCount={data.conversions?.pages || 0}
totalItems={data.conversions?.total || 0}
/>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.conversions?.items || []}
{columns}
pageCount={data.conversions?.pages || 0}
totalItems={data.conversions?.total || 0}
/>
</Card.Content>
</Card.Root>
<CreateDialogEdit
open={dialogOpen}
on:close={() => dialogOpen = false}
on:success={handleSuccess}
/>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -1,57 +1,53 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
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', ace_units: { 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();
// Note: apiUrl already ends with /
const endpoint = `${apiUrl}api/v1/a76/units-of-measure/ace?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 filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (!response.ok) {
console.error(`Error fetching ACE units: ${response.status} ${response.statusText}`);
if (code) filters.code = code;
if (description) filters.description = description;
// Obtener company_id de la cookie o usar el primero disponible
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
error: 'No se encontró una compañía seleccionada',
ace_units: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/units-of-measure/ace/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', ace_units: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { ace_units: await response.json() };
} catch (error) {
console.error('Error fetching ACE units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
console.error('Error loading ACE units:', error);
return { error: 'Error loading', ace_units: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/ace/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/ace/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
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';
@@ -46,10 +46,10 @@
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
data={data.ace_units?.items || []}
{columns}
pageCount={data.pages}
totalItems={data.total}
pageCount={data.ace_units?.pages || 0}
totalItems={data.ace_units?.total || 0}
/>
</Card.Content>
</Card.Root>

View File

@@ -1,56 +1,45 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
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', american_units: { 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/units-of-measure/american?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 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 parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/units-of-measure/american/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
console.error(`Error fetching American units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
return { error: 'Failed to load', american_units: { 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
};
return { american_units: await response.json() };
} catch (error) {
console.error('Error fetching American units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
console.error('Error loading American units:', error);
return { error: 'Error loading', american_units: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/american/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/american/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte'; // Reusing generic data table from ACE
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';
@@ -14,10 +14,10 @@
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
handleSuccess();
});
async function refreshData() {
async function handleSuccess() {
loading = true;
await invalidateAll();
loading = false;
@@ -33,7 +33,7 @@
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
@@ -44,18 +44,18 @@
</div>
<Card.Root>
<Card.Content class="p-0">
<Card.Content class="p-6">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
columns={columns}
data={data.american_units?.items || []}
pageCount={data.american_units?.pages || 0}
totalItems={data.american_units?.total || 0}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -1,56 +1,45 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
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', customs_units: { 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/units-of-measure/customs?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 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 parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/units-of-measure/customs/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
console.error(`Error fetching Customs units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
return { error: 'Failed to load', customs_units: { 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
};
return { customs_units: await response.json() };
} catch (error) {
console.error('Error fetching Customs units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Error al cargar datos'
};
console.error('Error loading Customs units:', error);
return { error: 'Error loading', customs_units: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,61 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/customs/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/customs/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/customs/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 { invalidateAll } from '$app/navigation';
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 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);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
// Función unificada para refrescar datos
async function handleSuccess() {
loading = true;
await invalidateAll();
loading = false;
}
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
// Definimos las columnas pasando el callback de éxito
const columns = createColumns(handleSuccess);
</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>
<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={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>
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Aduanas MEX</h2>
<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>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<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>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -1,56 +1,45 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
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', general_units: { 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/units-of-measure?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 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 parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/units-of-measure/general/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
console.error(`Error fetching General units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
return { error: 'Failed to load', general_units: { 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
};
return { general_units: await response.json() };
} catch (error) {
console.error('Error fetching General units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Error al cargar datos'
};
console.error('Error loading General units:', error);
return { error: 'Error loading', general_units: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/general/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/general/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/general/data-table.svelte';
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';
@@ -14,10 +14,10 @@
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
handleSuccess();
});
async function refreshData() {
async function handleSuccess() {
loading = true;
await invalidateAll();
loading = false;
@@ -33,7 +33,7 @@
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
@@ -44,18 +44,18 @@
</div>
<Card.Root>
<Card.Content class="p-0">
<Card.Content class="p-6">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
columns={columns}
data={data.general_units?.items || []}
pageCount={data.general_units?.pages || 0}
totalItems={data.general_units?.total || 0}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -1,56 +1,45 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
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', oma_units: { 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/units-of-measure/oma?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 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 parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(`v1/a76/units-of-measure/oma/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
console.error(`Error fetching OMA units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
return { error: 'Failed to load', oma_units: { 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
};
return { oma_units: await response.json() };
} catch (error) {
console.error('Error fetching OMA units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
console.error('Error loading OMA units:', error);
return { error: 'Error loading', oma_units: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/oma/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/oma/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte'; // Reusing generic data table from ACE
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';
@@ -14,10 +14,10 @@
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
handleSuccess();
});
async function refreshData() {
async function handleSuccess() {
loading = true;
await invalidateAll();
loading = false;
@@ -33,7 +33,7 @@
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<Button variant="outline" size="icon" onclick={handleSuccess} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
@@ -44,18 +44,18 @@
</div>
<Card.Root>
<Card.Content class="p-0">
<Card.Content class="p-6">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
columns={columns}
data={data.oma_units?.items || []}
pageCount={data.oma_units?.pages || 0}
totalItems={data.oma_units?.total || 0}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
onSuccess={handleSuccess}
/>
</div>