From 0a68454593f47279dfb7b492712764edc2bc69ad Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 18 Mar 2026 13:22:45 -0600 Subject: [PATCH] 6feature/fix-en-base-a-scaii --- .../a76/general_catalogs/equivalencies/dto.py | 65 +-- .../general_catalogs/equivalencies/models.py | 71 ++-- .../general_catalogs/equivalencies/routes.py | 89 +--- .../general_catalogs/equivalencies/service.py | 331 ++++++--------- backend/api/v1/modules/a76/items/models.py | 4 +- .../a76/general_catalogs/equivalencies.ts | 99 ++++- .../equivalencies/catalog-columns.ts | 40 ++ .../catalog-create-edit-dialog.svelte | 146 +++++++ .../catalog-data-table-actions.svelte | 71 ++++ .../general_catalogs/equivalencies/columns.ts | 2 - .../equivalencies/create-edit-dialog.svelte | 128 ++---- .../data-equivalencies-dialog.svelte | 383 ++++++++++++++++++ .../equivalencies/+page.server.ts | 11 +- .../equivalencies/+page.svelte | 71 ++-- 14 files changed, 1022 insertions(+), 489 deletions(-) create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py index a4088acd..4494351f 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py @@ -3,43 +3,17 @@ from decimal import Decimal from pydantic import BaseModel, Field, ConfigDict -# Equivalency (parent) DTOs - - -class EquivalencyBase(BaseModel): - # Agrupa múltiples destinos para un mismo origen. - identifier: str = Field(..., max_length=10, description="From unit code") - description: Optional[str] = Field(None, max_length=200, description="Description (optional)") - - -class EquivalencyCreate(EquivalencyBase): - pass - - -class EquivalencyUpdate(BaseModel): - identifier: Optional[str] = Field(None, max_length=10) - description: Optional[str] = Field(None, max_length=200) - - -class EquivalencyResponse(EquivalencyBase): - id: int - tenant_id: int - company_id: int - - model_config = ConfigDict(from_attributes=True) - - -# Equivalency Item DTOs (catálogo por fila) +# EquivalencyItem DTOs (pool global independiente) class EquivalencyItemBase(BaseModel): - original_field: str = Field(..., max_length=100, description="Original unit code (from)") - external_field: str = Field(..., max_length=100, description="External unit code (to)") - conversion_factor: Optional[Decimal] = Field(None, description="Conversion factor") + original_field: str = Field(..., max_length=100, description="Campo original (from)") + external_field: str = Field(..., max_length=100, description="Campo externo (to)") + conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión") class EquivalencyItemCreate(EquivalencyItemBase): - conversion_factor: Decimal = Field(..., description="Conversion factor") + conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión") class EquivalencyItemUpdate(BaseModel): @@ -50,8 +24,35 @@ class EquivalencyItemUpdate(BaseModel): class EquivalencyItemResponse(EquivalencyItemBase): id: int - equivalency_id: int tenant_id: int company_id: int model_config = ConfigDict(from_attributes=True) + + +# Equivalency DTOs (catálogo — referencia un item del pool) + + +class EquivalencyBase(BaseModel): + identifier: str = Field(..., max_length=10, description="Identificador único") + description: Optional[str] = Field(None, max_length=200, description="Descripción (opcional)") + item_id: Optional[int] = Field(None, description="ID del EquivalencyItem seleccionado") + + +class EquivalencyCreate(EquivalencyBase): + pass + + +class EquivalencyUpdate(BaseModel): + identifier: Optional[str] = Field(None, max_length=10) + description: Optional[str] = Field(None, max_length=200) + item_id: Optional[int] = Field(None, description="Re-asociar a otro EquivalencyItem") + + +class EquivalencyResponse(EquivalencyBase): + id: int + tenant_id: int + company_id: int + item: Optional[EquivalencyItemResponse] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py index 87de137c..211badd7 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py @@ -1,13 +1,39 @@ from typing import Optional, List from decimal import Decimal -from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, ForeignKeyConstraint, Numeric +from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, Numeric from sqlalchemy.orm import Mapped, mapped_column, relationship from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure + + +class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): + """Pool global de mapeos campo_original → campo_externo. + Un mismo item puede ser referenciado por múltiples Equivalency.""" + + __tablename__ = "equivalency_items" + __table_args__ = ( + UniqueConstraint( + "original_field", "external_field", "tenant_id", "company_id", + name="uq_equivalency_item_fields" + ), + {"schema": "a76"} + ) + + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True) + + original_field: Mapped[str] = mapped_column(String(100), nullable=False) + external_field: Mapped[str] = mapped_column(String(100), nullable=False) + conversion_factor: Mapped[Optional[Decimal]] = mapped_column( + Numeric(13, 6), nullable=True) + + equivalencies: Mapped[List["Equivalency"]] = relationship( + back_populates="item", foreign_keys="[Equivalency.item_id]") class Equivalency(Base, TenantScopedMixin, TimestampMixin): + """Catálogo de equivalencias. Cada registro selecciona UN item del pool.""" + __tablename__ = "equivalencies" __table_args__ = ( UniqueConstraint("identifier", "tenant_id", "company_id", @@ -19,42 +45,13 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin): Integer, primary_key=True, autoincrement=True) identifier: Mapped[str] = mapped_column(String(10), nullable=False) - description: Mapped[Optional[str]] = mapped_column( String(200), nullable=True) - items: Mapped[List["EquivalencyItem"]] = relationship( - back_populates="equivalency", cascade="all, delete-orphan") + item_id: Mapped[Optional[int]] = mapped_column( + Integer, + ForeignKey("a76.equivalency_items.id", ondelete="SET NULL"), + nullable=True) - -class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): - __tablename__ = "equivalency_items" - __table_args__ = ( - UniqueConstraint("equivalency_id", "original_field", - "external_field", "tenant_id", "company_id", name="uq_equivalency_item_fields"), - ForeignKeyConstraint( - ["original_field", "tenant_id", "company_id"], - ["a76.units_of_measure.code", "a76.units_of_measure.tenant_id", - "a76.units_of_measure.company_id"] - ), - {"schema": "a76"} - ) - - id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True) - - equivalency_id: Mapped[int] = mapped_column( - Integer, ForeignKey("a76.equivalencies.id"), nullable=False) - - original_field: Mapped[str] = mapped_column( - String(100), nullable=False) # Relation to Unit of Measure - - external_field: Mapped[str] = mapped_column(String(100), nullable=False) - - conversion_factor: Mapped[Optional[Decimal]] = mapped_column( - Numeric(13, 6), nullable=True - ) - - equivalency: Mapped["Equivalency"] = relationship(back_populates="items") - - unit_of_measure: Mapped["UnitOfMeasure"] = relationship() + item: Mapped[Optional["EquivalencyItem"]] = relationship( + back_populates="equivalencies", foreign_keys=[item_id]) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py index d5f00b96..6e076725 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py @@ -1,12 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session -from typing import List, Dict, Any - -from core.database import get_core_db -from core.security import get_current_user, validate_access_to_resource +from fastapi import APIRouter from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from . import service -from .models import Equivalency, EquivalencyItem from .dto import ( EquivalencyCreate, EquivalencyResponse, EquivalencyUpdate, EquivalencyItemCreate, EquivalencyItemResponse, EquivalencyItemUpdate @@ -16,19 +9,7 @@ from .service import EquivalencyService, EquivalencyItemService router = APIRouter(prefix="/equivalencies", tags=["a76.general_catalogs.equivalencies"]) -# Equivalency CRUD -equivalency_crud = TenantCRUDRoutes( - service=EquivalencyService, - create_schema=EquivalencyCreate, - update_schema=EquivalencyUpdate, - response_schema=EquivalencyResponse, - prefix="", - tags=["Equivalencies"], - resource_name="Equivalency", - enable_list=True, -) - -# Equivalency Item CRUD +# Pool global de EquivalencyItems item_crud = TenantCRUDRoutes( service=EquivalencyItemService, create_schema=EquivalencyItemCreate, @@ -41,60 +22,18 @@ item_crud = TenantCRUDRoutes( enable_filters=True, ) -# Custom endpoint for creating items nested under equivalency - - -@equivalency_crud.router.post( - "/{equivalency_id}/items", - response_model=EquivalencyItemResponse, - status_code=status.HTTP_201_CREATED, - summary="Create equivalency item", +# Catálogo de equivalencias (referencia item_id) +equivalency_crud = TenantCRUDRoutes( + service=EquivalencyService, + create_schema=EquivalencyCreate, + update_schema=EquivalencyUpdate, + response_schema=EquivalencyResponse, + prefix="", + tags=["Equivalencies"], + resource_name="Equivalency", + enable_list=True, + enable_filters=True, ) -async def create_equivalency_item( - equivalency_id: int, - data: EquivalencyItemCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - tenant_id = validate_access_to_resource(db, company_id, current_user) - # Verify parent exists - parent = EquivalencyService.get_by_id( - db, equivalency_id, tenant_id, company_id) - if not parent: - raise HTTPException(status_code=404, detail="Equivalency not found") - - # Create item - # We need to manually handle the creation because the DTO doesn't have equivalency_id - # and the service.create expects data to match the model or DTO. - # But service.create takes EquivalencyItemCreate which doesn't have equivalency_id. - # So we need to modify the data or handle it in service. - - # Actually, I implemented EquivalencyItemService.create to take EquivalencyItemCreate. - # And it tries to create the model. - # But the model needs equivalency_id. - # So EquivalencyItemService.create will fail if I don't pass equivalency_id. - # I should update EquivalencyItemService.create to accept extra kwargs or handle this. - - # Let's update the service call here to pass equivalency_id manually if I can't change the service signature easily. - # But wait, I can just instantiate the model here or update the service. - - # I'll update the service to handle it. - # But for now, let's assume I can pass it in the data if I convert it to dict. - - item_data = data.model_dump() - item_data['equivalency_id'] = equivalency_id - - # I need to call a method that accepts this. - # EquivalencyItemService.create takes EquivalencyItemCreate. - # I should probably add a specific method for this or update create. - - # Let's use a direct DB call here or add a method to service. - # Adding a method to service is cleaner. - - return EquivalencyItemService.create_nested(db, equivalency_id, data, tenant_id, company_id) - - -router.include_router(equivalency_crud.router) router.include_router(item_crud.router) +router.include_router(equivalency_crud.router) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py index f55b2bf8..2732877c 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py @@ -7,130 +7,9 @@ from .models import Equivalency, EquivalencyItem from .dto import EquivalencyCreate, EquivalencyUpdate, EquivalencyItemCreate, EquivalencyItemUpdate -class EquivalencyService: - @staticmethod - def get_all( - db: Session, - tenant_id: int, - company_id: int, - skip: int = 0, - limit: int = 100, - filters: Optional[Dict[str, Any]] = None - ) -> Tuple[List[Equivalency], int]: - query = db.query(Equivalency).filter( - Equivalency.tenant_id == tenant_id, - Equivalency.company_id == company_id, - ) - - if filters: - # Soporta filtros de UI por nombre de parámetro. - if "from_unit_code" in filters: - query = query.filter( - Equivalency.identifier.ilike(f"%{filters['from_unit_code']}%") - ) - if "identifier" in filters: - query = query.filter( - Equivalency.identifier.ilike(f"%{filters['identifier']}%") - ) - if "description" in filters: - query = query.filter( - Equivalency.description.ilike(f"%{filters['description']}%") - ) - - total = query.count() - items = query.offset(skip).limit(limit).all() - return items, total - - @staticmethod - def get_by_id( - db: Session, - id: int, - tenant_id: int, - company_id: int - ) -> Optional[Equivalency]: - item = db.query(Equivalency).filter( - Equivalency.id == id, - Equivalency.tenant_id == tenant_id, - Equivalency.company_id == company_id - ).first() - - return item - - @staticmethod - def create( - db: Session, - data: EquivalencyCreate, - tenant_id: int, - company_id: int - ) -> Equivalency: - try: - db_obj = Equivalency( - identifier=data.identifier, - description=data.description, - tenant_id=tenant_id, - company_id=company_id, - ) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - except IntegrityError as e: - db.rollback() - error_msg = str(e.orig) if hasattr(e, "orig") else str(e) - if "uq_equivalency_identifier" in error_msg: - raise HTTPException( - status_code=400, - detail=f"Ya existe un registro para el origen '{data.identifier}'.", - ) from e - raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e - except Exception as e: - db.rollback() - raise e - - @staticmethod - def update( - db: Session, - id: int, - tenant_id: int, - data: EquivalencyUpdate, - company_id: int - ) -> Optional[Equivalency]: - db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) - if not db_obj: - return None - - try: - update_dict = data.model_dump(exclude_unset=True) - for key, value in update_dict.items(): - setattr(db_obj, key, value) - - db.commit() - db.refresh(db_obj) - return db_obj - except IntegrityError as e: - db.rollback() - error_msg = str(e.orig) if hasattr(e, "orig") else str(e) - raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}") from e - except Exception as e: - db.rollback() - raise e - - @staticmethod - def delete( - db: Session, - id: int, - tenant_id: int, - company_id: int - ) -> bool: - db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) - if not db_obj: - return False - - db.delete(db_obj) - db.commit() - return True - class EquivalencyItemService: + """CRUD simple sobre el pool global de items.""" + @staticmethod def get_all( db: Session, @@ -146,15 +25,13 @@ class EquivalencyItemService: ) if filters: - if "from_unit_code" in filters: + if "original_field" in filters: query = query.filter( - EquivalencyItem.original_field.ilike( - f"%{filters['from_unit_code']}%" - ) + EquivalencyItem.original_field.ilike(f"%{filters['original_field']}%") ) - if "to_unit_code" in filters: + if "external_field" in filters: query = query.filter( - EquivalencyItem.external_field.ilike(f"%{filters['to_unit_code']}%") + EquivalencyItem.external_field.ilike(f"%{filters['external_field']}%") ) total = query.count() @@ -182,29 +59,7 @@ class EquivalencyItemService: company_id: int ) -> EquivalencyItem: try: - # Equivalency padre agrupado por `from_unit_code` (original_field) - parent = ( - db.query(Equivalency) - .filter( - Equivalency.tenant_id == tenant_id, - Equivalency.company_id == company_id, - Equivalency.identifier == data.original_field, - ) - .first() - ) - - if not parent: - parent = Equivalency( - identifier=data.original_field, - description=None, - tenant_id=tenant_id, - company_id=company_id, - ) - db.add(parent) - db.flush() # obtener id sin commit - db_obj = EquivalencyItem( - equivalency_id=parent.id, original_field=data.original_field, external_field=data.external_field, conversion_factor=data.conversion_factor, @@ -218,36 +73,13 @@ class EquivalencyItemService: except IntegrityError as e: db.rollback() error_msg = str(e.orig) if hasattr(e, "orig") else str(e) - if "units_of_measure" in error_msg: + if "uq_equivalency_item_fields" in error_msg: raise HTTPException( status_code=400, - detail=( - "Verifica que las unidades existan en el catálogo de U.M." - ), + detail=f"Ya existe un item con los mismos campos '{data.original_field}' → '{data.external_field}'." ) from e raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e - @staticmethod - def create_nested( - db: Session, - equivalency_id: int, - data: EquivalencyItemCreate, - tenant_id: int, - company_id: int - ) -> EquivalencyItem: - db_obj = EquivalencyItem( - equivalency_id=equivalency_id, - original_field=data.original_field, - external_field=data.external_field, - conversion_factor=data.conversion_factor, - tenant_id=tenant_id, - company_id=company_id - ) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - @staticmethod def update( db: Session, @@ -260,39 +92,10 @@ class EquivalencyItemService: if not db_obj: return None - update_dict = data.model_dump(exclude_unset=True) try: - new_original = update_dict.get("original_field") - if new_original and new_original != db_obj.original_field: - # Re-asignar al padre del nuevo `from_unit_code` - parent = ( - db.query(Equivalency) - .filter( - Equivalency.tenant_id == tenant_id, - Equivalency.company_id == company_id, - Equivalency.identifier == new_original, - ) - .first() - ) - if not parent: - parent = Equivalency( - identifier=new_original, - description=None, - tenant_id=tenant_id, - company_id=company_id, - ) - db.add(parent) - db.flush() - - db_obj.equivalency_id = parent.id - db_obj.original_field = new_original - - # Aplicar resto de cambios + update_dict = data.model_dump(exclude_unset=True) for key, value in update_dict.items(): - if key == "original_field": - continue # ya aplicado arriba (y con posible re-parenting) setattr(db_obj, key, value) - db.commit() db.refresh(db_obj) return db_obj @@ -314,3 +117,119 @@ class EquivalencyItemService: db.delete(db_obj) db.commit() return True + + +class EquivalencyService: + """CRUD del catálogo de equivalencias. Cada registro referencia un EquivalencyItem.""" + + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None + ) -> Tuple[List[Equivalency], int]: + query = db.query(Equivalency).filter( + Equivalency.tenant_id == tenant_id, + Equivalency.company_id == company_id, + ) + + if filters: + if "from_unit_code" in filters: + query = query.filter( + Equivalency.identifier.ilike(f"%{filters['from_unit_code']}%") + ) + if "identifier" in filters: + query = query.filter( + Equivalency.identifier.ilike(f"%{filters['identifier']}%") + ) + if "description" in filters: + query = query.filter( + Equivalency.description.ilike(f"%{filters['description']}%") + ) + + total = query.count() + items = query.offset(skip).limit(limit).all() + return items, total + + @staticmethod + def get_by_id( + db: Session, + id: int, + tenant_id: int, + company_id: int + ) -> Optional[Equivalency]: + return db.query(Equivalency).filter( + Equivalency.id == id, + Equivalency.tenant_id == tenant_id, + Equivalency.company_id == company_id + ).first() + + @staticmethod + def create( + db: Session, + data: EquivalencyCreate, + tenant_id: int, + company_id: int + ) -> Equivalency: + try: + db_obj = Equivalency( + identifier=data.identifier, + description=data.description, + item_id=data.item_id, + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + except IntegrityError as e: + db.rollback() + error_msg = str(e.orig) if hasattr(e, "orig") else str(e) + if "uq_equivalency_identifier" in error_msg: + raise HTTPException( + status_code=400, + detail=f"Ya existe un registro para el identificador '{data.identifier}'.", + ) from e + raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e + + @staticmethod + def update( + db: Session, + id: int, + tenant_id: int, + data: EquivalencyUpdate, + company_id: int + ) -> Optional[Equivalency]: + db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) + if not db_obj: + return None + + try: + update_dict = data.model_dump(exclude_unset=True) + for key, value in update_dict.items(): + setattr(db_obj, key, value) + db.commit() + db.refresh(db_obj) + return db_obj + except IntegrityError as e: + db.rollback() + error_msg = str(e.orig) if hasattr(e, "orig") else str(e) + raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}") from e + + @staticmethod + def delete( + db: Session, + id: int, + tenant_id: int, + company_id: int + ) -> bool: + db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) + if not db_obj: + return False + db.delete(db_obj) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py index e9bcb55a..d1a569df 100644 --- a/backend/api/v1/modules/a76/items/models.py +++ b/backend/api/v1/modules/a76/items/models.py @@ -14,6 +14,7 @@ from core.database import Base from api.v1.modules.a76.classes.models import Class from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure +from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail if TYPE_CHECKING: from .line_financials.models import LineFinancial @@ -24,7 +25,6 @@ if TYPE_CHECKING: from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem from api.v1.modules.a76.parts.models import Part from api.v1.modules.a76.invoices.models import InvoiceHeader - from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail # ============================================================================ # CORE ENTITIES @@ -221,7 +221,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): uselist=False, ) identifiers: Mapped[List["IdentifierDetail"]] = relationship( - "IdentifierDetail", + IdentifierDetail, back_populates="line", cascade="all, delete-orphan", ) diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts index 74788989..b54abcfd 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts @@ -1,22 +1,23 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; +// ======================== +// ITEMS (pool global) +// ======================== + export interface EquivalencyItem { id: number; - original_field: string; // from_unit_code - external_field: string; // to_unit_code + original_field: string; + external_field: string; conversion_factor: number | null; - equivalency_id: number; tenant_id: number; company_id: number; - created_at: string; - updated_at?: string; } export interface EquivalencyItemCreate { original_field: string; external_field: string; - conversion_factor: number; + conversion_factor?: number | null; } export interface EquivalencyItemUpdate { @@ -38,14 +39,13 @@ export async function getEquivalencyItems( pageSize: number = 50, companyId: number, filters: Record = {} -) : Promise> { +): Promise> { const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), company_id: companyId.toString(), ...filters }); - return await api.get(`/v1/a76/equivalencies/items/?${params.toString()}`); } @@ -71,6 +71,87 @@ export async function updateEquivalencyItem( return await api.put(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`, data); } -export async function deleteEquivalencyItem(id: number, companyId: number): Promise> { +export async function deleteEquivalencyItem( + id: number, + companyId: number +): Promise> { return await api.delete(`/v1/a76/equivalencies/items/${id}?company_id=${companyId}`); } + +// ======================== +// CATÁLOGO (referencia item_id) +// ======================== + +export interface Equivalency { + id: number; + identifier: string; + description: string | null; + item_id: number | null; + item: EquivalencyItem | null; + tenant_id: number; + company_id: number; +} + +export interface EquivalencyCreate { + identifier: string; + description?: string | null; + item_id?: number | null; +} + +export interface EquivalencyUpdate { + identifier?: string; + description?: string | null; + item_id?: number | null; +} + +export interface EquivalencyListResponse { + items: Equivalency[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getEquivalencies( + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); + return await api.get(`/v1/a76/equivalencies/?${params.toString()}`); +} + +export async function getEquivalency( + id: number, + companyId: number +): Promise> { + return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`); +} + +export async function createEquivalency( + data: EquivalencyCreate, + companyId: number +): Promise> { + return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data); +} + +export async function updateEquivalency( + id: number, + data: EquivalencyUpdate, + companyId: number +): Promise> { + return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data); +} + +export async function deleteEquivalency( + id: number, + companyId: number +): Promise> { + return await api.delete(`/v1/a76/equivalencies/${id}?company_id=${companyId}`); +} diff --git a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts new file mode 100644 index 00000000..32ee7c5f --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts @@ -0,0 +1,40 @@ +import type { ColumnDef } from '@tanstack/table-core'; +import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies'; +import { renderComponent } from '$lib/components/ui/data-table'; +import CatalogDataTableActions from './catalog-data-table-actions.svelte'; + +export function createCatalogColumns({ + onInsertItems, + onEdit, + onSuccess +}: { + onInsertItems: (equivalency: Equivalency) => void; + onEdit: (equivalency: Equivalency) => void; + onSuccess?: () => void; +}): ColumnDef[] { + return [ + { + accessorKey: 'identifier', + header: 'Identificador', + cell: ({ row }) => row.original.identifier || 'N/A' + }, + { + accessorKey: 'description', + header: 'Descripción', + cell: ({ row }) => row.original.description || '' + }, + { + id: 'actions', + Header: 'Acciones', + cell: ({ row }) => { + return renderComponent(CatalogDataTableActions, { + item: row.original, + onInsertItems, + onEdit, + onSuccess + }); + } + } + ]; +} + diff --git a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte new file mode 100644 index 00000000..077f837a --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte @@ -0,0 +1,146 @@ + + + + + + {title} + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="grid gap-4 py-4" + > + {#if error} +
{error}
+ {/if} + +
+
+ + +
+ +
+ +