6feature/fix-en-base-a-scaii

This commit is contained in:
2026-03-18 13:22:45 -06:00
parent 8b9537b5a9
commit 0a68454593
14 changed files with 1022 additions and 489 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, any> = {}
) : Promise<ApiResponse<EquivalencyItemListResponse>> {
): Promise<ApiResponse<EquivalencyItemListResponse>> {
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<ApiResponse<void>> {
export async function deleteEquivalencyItem(
id: number,
companyId: number
): Promise<ApiResponse<void>> {
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<string, any> = {}
): Promise<ApiResponse<EquivalencyListResponse>> {
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<ApiResponse<Equivalency>> {
return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
}
export async function createEquivalency(
data: EquivalencyCreate,
companyId: number
): Promise<ApiResponse<Equivalency>> {
return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data);
}
export async function updateEquivalency(
id: number,
data: EquivalencyUpdate,
companyId: number
): Promise<ApiResponse<Equivalency>> {
return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data);
}
export async function deleteEquivalency(
id: number,
companyId: number
): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/equivalencies/${id}?company_id=${companyId}`);
}

View File

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

View File

@@ -0,0 +1,146 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Textarea } from '$lib/components/ui/textarea';
import { companyStore } from '$lib/stores/company.svelte';
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import {
createEquivalency,
updateEquivalency
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
let {
open = $bindable(false),
item = null,
onSuccess
}: {
open: boolean;
item?: Equivalency | null;
onSuccess?: (saved?: Equivalency) => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? 'Editar equivalencia' : 'Nueva equivalencia');
let formData = $state({
identifier: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (!open) return;
if (item) {
formData = {
identifier: item.identifier || '',
description: item.description || ''
};
} else {
formData = {
identifier: '',
description: ''
};
}
error = null;
});
async function handleSubmit() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
loading = true;
error = null;
try {
if (!formData.identifier.trim()) throw new Error('Identificador requerido');
if (formData.identifier.trim().length > 10)
throw new Error('El identificador no puede exceder 10 caracteres');
const dataToSend = {
identifier: formData.identifier.trim(),
description: formData.description.trim() ? formData.description.trim() : null
};
const response = isEdit && item
? await updateEquivalency(item.id, dataToSend, companyId)
: await createEquivalency(dataToSend, companyId);
if (response.error) throw new Error(response.error);
open = false;
// Proceso 1: si fue INSERT, encadenamos a DATOS DE EQUIVALENTES.
if (!isEdit && response.data) onSuccess?.(response.data);
else onSuccess?.();
} catch (e) {
console.error(e);
error = e instanceof Error ? e.message : 'Error al guardar la equivalencia';
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[520px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
class="grid gap-4 py-4"
>
{#if error}
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">{error}</div>
{/if}
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="identifier">
Identificador <span class="text-destructive">*</span>
</Label>
<Input
id="identifier"
bind:value={formData.identifier}
disabled={loading}
maxlength={10}
required
placeholder="Ej: KGM, PCS, MP..."
/>
</div>
<div class="grid gap-2">
<Label for="description">Descripción</Label>
<Textarea
id="description"
bind:value={formData.description}
disabled={loading}
maxlength={200}
class="min-h-[100px]"
placeholder="(opcional)"
/>
</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,71 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
import { companyStore } from '$lib/stores/company.svelte';
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { deleteEquivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
let {
item,
onInsertItems,
onEdit,
onSuccess
}: {
item: Equivalency;
onInsertItems: (equivalency: Equivalency) => void;
onEdit: (equivalency: Equivalency) => void;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
async function handleDelete() {
if (!confirm('¿Está seguro de eliminar esta equivalencia?')) return;
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
loading = true;
try {
const response = await deleteEquivalency(item.id, companyId);
if (response.error) throw new Error(response.error);
onSuccess?.();
} catch (err: any) {
console.error('Error deleting equivalency:', err);
alert(err?.message || 'Error al eliminar la equivalencia');
} 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={() => onInsertItems(item)}>
<span>Insertar items</span>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => onEdit(item)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Borrar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -2,8 +2,6 @@ import type { ColumnDef } from '@tanstack/table-core';
import type { EquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { Header } from '$lib/components/ui/alert-dialog';
export function createColumns(onSuccess?: () => void): ColumnDef<EquivalencyItem>[] {
return [
{

View File

@@ -4,8 +4,7 @@
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { companyStore } from '$lib/stores/company.svelte';
import UnitMeasureSelectorDialog from '$lib/components/dashboard/goods/modales/unit-measure-dialog.svelte';
import { FolderSearch, Scale } from 'lucide-svelte';
import { Scale } from 'lucide-svelte';
import {
createEquivalencyItem,
updateEquivalencyItem,
@@ -15,49 +14,42 @@
let {
open = $bindable(false),
item = null,
defaultOriginalField = null,
onSuccess
}: {
open: boolean;
item?: EquivalencyItem | null;
defaultOriginalField?: string | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(
isEdit
? `Editar Equivalencia ${item?.original_field || ''}${item?.external_field || ''}`
: 'Nueva Equivalencia'
? `Editar: ${item?.original_field || ''}${item?.external_field || ''}`
: 'Insertar detalle de equivalencia'
);
let formData = $state({
original_field: '',
external_field: '',
conversion_factor: ''
external_field: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
let showFromUomModal = $state(false);
let showToUomModal = $state(false);
$effect(() => {
if (!open) return;
if (item) {
formData = {
original_field: item.original_field || '',
external_field: item.external_field || '',
conversion_factor:
item.conversion_factor !== null && item.conversion_factor !== undefined
? item.conversion_factor.toString()
: ''
external_field: item.external_field || ''
};
} else {
formData = {
original_field: '',
external_field: '',
conversion_factor: ''
original_field: defaultOriginalField ?? '',
external_field: ''
};
}
@@ -75,18 +67,12 @@
error = null;
try {
if (!formData.original_field.trim()) throw new Error('Código origen requerido');
if (!formData.external_field.trim()) throw new Error('Código destino requerido');
if (formData.conversion_factor === '' || formData.conversion_factor === null)
throw new Error('Factor de conversión requerido');
const factor = parseFloat(formData.conversion_factor);
if (isNaN(factor)) throw new Error('El factor de conversión debe ser un número válido');
if (!formData.original_field.trim()) throw new Error('Campo original requerido');
if (!formData.external_field.trim()) throw new Error('Campo externo requerido');
const dataToSend = {
original_field: formData.original_field.trim(),
external_field: formData.external_field.trim(),
conversion_factor: factor
external_field: formData.external_field.trim()
};
if (isEdit && item) {
@@ -128,78 +114,40 @@
<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>
Campo Original <span class="text-destructive">*</span>
</Label>
<div class="flex gap-2">
<div class="relative w-full">
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
<Input
id="from_unit_code"
bind:value={formData.original_field}
readonly
onclick={() => (showFromUomModal = true)}
class="cursor-pointer pl-9 font-mono"
placeholder="Seleccione..."
disabled={loading}
required
/>
</div>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showFromUomModal = true)}
<Input
id="from_unit_code"
bind:value={formData.original_field}
class="pl-9 font-mono"
placeholder="Ej: PZA, KGM..."
disabled={loading}
class="shrink-0"
>
<FolderSearch class="h-4 w-4" />
</Button>
required
/>
</div>
</div>
</div>
<div class="grid gap-2">
<Label for="to_unit_code">
Código destino <span class="text-destructive">*</span>
Campo Exterior <span class="text-destructive">*</span>
</Label>
<div class="flex gap-2">
<div class="relative w-full">
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
<Input
id="to_unit_code"
bind:value={formData.external_field}
readonly
onclick={() => (showToUomModal = true)}
class="cursor-pointer pl-9 font-mono"
placeholder="Seleccione..."
disabled={loading}
required
/>
</div>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showToUomModal = true)}
disabled={loading}
class="shrink-0"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</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
/>
id="to_unit_code"
bind:value={formData.external_field}
class="pl-9 font-mono"
placeholder="Ej: PIEZAS, KGS..."
disabled={loading}
required
/>
</div>
</div>
</div>
</div>
@@ -212,17 +160,3 @@
</form>
</Dialog.Content>
</Dialog.Root>
<UnitMeasureSelectorDialog
bind:open={showFromUomModal}
onSelect={(u) => {
formData.original_field = u.code;
}}
/>
<UnitMeasureSelectorDialog
bind:open={showToUomModal}
onSelect={(u) => {
formData.external_field = u.code;
}}
/>

View File

@@ -0,0 +1,383 @@
<script lang="ts">
import { untrack } from 'svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Table from '$lib/components/ui/table';
import { companyStore } from '$lib/stores/company.svelte';
import { toast } from 'svelte-sonner';
import { Check } from 'lucide-svelte';
import type {
Equivalency,
EquivalencyItem
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import {
getEquivalencyItems,
getEquivalencies,
createEquivalency,
deleteEquivalencyItem,
updateEquivalency
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import EquivalencyItemCreateEditDialog from './create-edit-dialog.svelte';
let {
open = $bindable(false),
equivalency = null,
mode = 'edit',
onSuccess
}: {
open: boolean;
equivalency?: Equivalency | null;
mode?: 'create' | 'edit';
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let items = $state<EquivalencyItem[]>([]);
// El item actualmente seleccionado/asociado a esta equivalencia
let selectedItemId = $state<number | null>(null);
let equivalencyDraft = $state({ identifier: '', description: '' });
let descriptionDraft = $state('');
let savedEquivalency = $state<Equivalency | null>(null);
function getEffectiveEquivalency(): Equivalency | null {
if (mode === 'edit') return equivalency ?? null;
return savedEquivalency;
}
// Modal de crear/editar item
let inputsOpen = $state(false);
let inputsItem = $state<EquivalencyItem | null>(null);
function getSelectedItem(): EquivalencyItem | null {
if (selectedItemId === null) return null;
return items.find((i) => i.id === selectedItemId) || null;
}
async function reloadItems() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
items = [];
return;
}
loading = true;
error = null;
try {
const resp = await getEquivalencyItems(1, 2000, companyId);
if (resp.error) throw new Error(resp.error);
items = resp.data.items || [];
} catch (e) {
console.error(e);
error = e instanceof Error ? e.message : 'Error al cargar items';
items = [];
} finally {
loading = false;
}
}
async function ensureEquivalencySaved(): Promise<Equivalency | null> {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
toast.error('Selecciona una compañía');
return null;
}
if (mode === 'edit') {
return equivalency ?? null;
}
if (savedEquivalency) return savedEquivalency;
const identifier = equivalencyDraft.identifier.trim();
if (!identifier) {
toast.error('El identificador es requerido');
return null;
}
const description = descriptionDraft.trim() || equivalencyDraft.description.trim() || null;
const created = await createEquivalency({ identifier, description }, companyId);
if (created.data && !created.error) {
savedEquivalency = created.data;
descriptionDraft = created.data.description || '';
return savedEquivalency;
}
// Si ya existía, resolver por identifier
const listResp = await getEquivalencies(1, 50, companyId, { from_unit_code: identifier });
const existing =
(listResp.data?.items || []).find(
(e) => e.identifier?.toUpperCase() === identifier.toUpperCase()
) ?? null;
if (!existing) {
toast.error(listResp.error || 'No se pudo crear la equivalencia');
return null;
}
savedEquivalency = existing;
descriptionDraft = existing.description || '';
return savedEquivalency;
}
async function handleSave() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
if (mode === 'create') {
const identifier = equivalencyDraft.identifier.trim();
if (!identifier) {
toast.error('El identificador es requerido');
return;
}
const description = descriptionDraft.trim() || null;
if (!savedEquivalency) {
const created = await createEquivalency(
{ identifier, description, item_id: selectedItemId },
companyId
);
if (created.error || !created.data) {
toast.error(created.error || 'Error al guardar');
return;
}
savedEquivalency = created.data;
} else {
const updated = await updateEquivalency(
savedEquivalency.id,
{ identifier, description, item_id: selectedItemId },
companyId
);
if (updated.error) {
toast.error(updated.error || 'Error al actualizar');
return;
}
}
} else {
if (!equivalency) return;
const updated = await updateEquivalency(
equivalency.id,
{ description: descriptionDraft.trim() || null, item_id: selectedItemId },
companyId
);
if (updated.error) {
toast.error(updated.error || 'Error al actualizar');
return;
}
}
toast.success('Equivalencia guardada');
open = false;
onSuccess?.();
}
async function handleInsert() {
const eq = await ensureEquivalencySaved();
if (!eq) return;
inputsItem = null;
inputsOpen = true;
}
async function handleEdit() {
const selected = getSelectedItem();
if (!selected) {
toast.error('Selecciona un item para editar');
return;
}
inputsItem = selected;
inputsOpen = true;
}
async function handleDelete() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const selected = getSelectedItem();
if (!selected) {
toast.error('Selecciona un item para borrar');
return;
}
if (!confirm('¿Está seguro de eliminar este item del pool?')) return;
try {
await deleteEquivalencyItem(selected.id, companyId);
toast.success('Item eliminado');
// Si era el item asociado a esta equivalencia, desvincular
if (selectedItemId === selected.id) selectedItemId = null;
await reloadItems();
onSuccess?.();
} catch (e) {
console.error(e);
toast.error('No se pudo eliminar', { description: e instanceof Error ? e.message : '' });
}
}
$effect(() => {
if (!open) return;
const _mode = mode;
const _equivalency = equivalency;
untrack(() => {
savedEquivalency = null;
if (_mode === 'edit') {
descriptionDraft = _equivalency?.description || '';
equivalencyDraft = {
identifier: _equivalency?.identifier || '',
description: _equivalency?.description || ''
};
savedEquivalency = _equivalency ?? null;
selectedItemId = _equivalency?.item_id ?? null;
} else {
equivalencyDraft = { identifier: '', description: '' };
descriptionDraft = '';
selectedItemId = null;
}
reloadItems();
});
});
function handleInputsSuccess() {
inputsOpen = false;
reloadItems();
onSuccess?.();
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[900px]">
<Dialog.Header>
<Dialog.Title>DATOS DE EQUIVALENTES</Dialog.Title>
<Dialog.Description>
Selecciona un item del pool para asociarlo a esta equivalencia.
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-3 gap-4">
<div class="grid gap-2">
<Label>Identificador</Label>
{#if mode === 'edit'}
<Input value={equivalency?.identifier || ''} readonly />
{:else if savedEquivalency}
<Input value={savedEquivalency.identifier || ''} readonly />
{:else}
<Input
bind:value={equivalencyDraft.identifier}
placeholder="Ej: PZA, KGS, MP..."
/>
{/if}
</div>
<div class="grid gap-2 col-span-2">
<Label>Descripción</Label>
<Input bind:value={descriptionDraft} placeholder="(opcional)" />
</div>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row class="bg-muted/20">
<Table.Head class="w-10"></Table.Head>
<Table.Head>Campo Original</Table.Head>
<Table.Head>Campo Externo</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if loading}
<Table.Row>
<Table.Cell colspan={3} class="h-24 text-center">Cargando...</Table.Cell>
</Table.Row>
{:else if error}
<Table.Row>
<Table.Cell colspan={3} class="h-24 text-center text-destructive">
{error}
</Table.Cell>
</Table.Row>
{:else if items.length === 0}
<Table.Row>
<Table.Cell colspan={3} class="h-24 text-center text-muted-foreground">
No hay items en el pool. Usa "Insertar" para crear uno.
</Table.Cell>
</Table.Row>
{:else}
{#each items as item}
{@const isSelected = item.id === selectedItemId}
<Table.Row
class={[
'cursor-pointer transition-colors',
isSelected
? 'bg-primary/10 font-medium'
: 'hover:bg-muted/50 text-muted-foreground'
].join(' ')}
onclick={() => (selectedItemId = isSelected ? null : item.id)}
>
<Table.Cell class="w-10 text-center">
{#if isSelected}
<Check class="h-4 w-4 text-primary mx-auto" />
{/if}
</Table.Cell>
<Table.Cell class="font-mono">{item.original_field}</Table.Cell>
<Table.Cell class="font-mono">{item.external_field}</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</div>
<div class="flex justify-between items-center">
<p class="text-xs text-muted-foreground">
{#if selectedItemId}
Item seleccionado: <span class="font-mono font-medium">
{items.find((i) => i.id === selectedItemId)?.original_field ?? ''}
{items.find((i) => i.id === selectedItemId)?.external_field ?? ''}
</span>
{:else}
Ningún item seleccionado para esta equivalencia.
{/if}
</p>
<div class="flex gap-2">
<Button variant="outline" size="sm" onclick={handleInsert}>Insertar</Button>
<Button
variant="outline"
size="sm"
onclick={handleEdit}
disabled={selectedItemId === null}
>
Editar
</Button>
<Button
variant="outline"
size="sm"
onclick={handleDelete}
disabled={selectedItemId === null}
>
Borrar
</Button>
</div>
</div>
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cerrar</Button>
<Button type="button" onclick={handleSave}>Guardar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<EquivalencyItemCreateEditDialog
bind:open={inputsOpen}
item={inputsItem}
defaultOriginalField={inputsItem ? null : null}
onSuccess={handleInputsSuccess}
/>

View File

@@ -24,11 +24,9 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
}
const filters: Record<string, string> = {};
// El backend usa `from_unit_code` para filtrar por `Equivalency.identifier`
const fromUnitCode = url.searchParams.get('from_unit_code');
const toUnitCode = url.searchParams.get('to_unit_code');
if (fromUnitCode) filters.from_unit_code = fromUnitCode;
if (toUnitCode) filters.to_unit_code = toUnitCode;
const queryParams = new URLSearchParams({
page: page.toString(),
@@ -37,7 +35,12 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
...filters
});
const response = await authenticatedFetch(`v1/a76/equivalencies/items/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
const response = await authenticatedFetch(
`v1/a76/equivalencies/?${queryParams.toString()}`,
{ method: 'GET' },
cookies,
fetch
);
if (!response.ok) {
return { error: 'Failed to load', equivalencies: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };

View File

@@ -2,32 +2,38 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/general_catalogs/equivalencies/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/equivalencies/create-edit-dialog.svelte';
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { createCatalogColumns } from '$lib/components/dashboard/general_catalogs/equivalencies/catalog-columns';
import DataTable from '$lib/components/dashboard/general_catalogs/equivalencies/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaEquivalencias } from '$lib/config/shortcuts/dashboard/general_catalogs/equivalencies/list';
import DataEquivalenciesDialog from '$lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let dataDialogOpen = $state(false);
let selectedEquivalency = $state<Equivalency | null>(null);
let dataMode = $state<'create' | 'edit'>('edit');
// Atajos
useShortcuts(
'Lista Equivalencias',
obtenerAtajosListaEquivalencias({
manejarNuevo: () => (createDialogOpen = true),
manejarNuevo: () => {
selectedEquivalency = null;
dataMode = 'create';
dataDialogOpen = true;
},
manejarActualizar: handleSuccess
})
);
// Filtros
let searchFrom = $state($page.url.searchParams.get('from_unit_code') || '');
let searchTo = $state($page.url.searchParams.get('to_unit_code') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
@@ -38,9 +44,6 @@
if (searchFrom) url.searchParams.set('from_unit_code', searchFrom);
else url.searchParams.delete('from_unit_code');
if (searchTo) url.searchParams.set('to_unit_code', searchTo);
else url.searchParams.delete('to_unit_code');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
@@ -50,6 +53,18 @@
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
function handleOpenInsertItems(equivalency: Equivalency) {
selectedEquivalency = equivalency;
dataMode = 'edit';
dataDialogOpen = true;
}
function handleOpenEditCatalog(equivalency: Equivalency) {
selectedEquivalency = equivalency;
dataMode = 'edit';
dataDialogOpen = true;
}
</script>
<div class="flex flex-col gap-4 p-4">
@@ -58,7 +73,13 @@
<h1 class="text-2xl font-bold tracking-tight">Equivalencias</h1>
<p class="text-muted-foreground">Catálogo de equivalencias</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Button
onclick={() => {
selectedEquivalency = null;
dataMode = 'create';
dataDialogOpen = true;
}}
>
<Plus class="mr-2 h-4 w-4" />
Nueva Equivalencia
</Button>
@@ -72,23 +93,23 @@
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código destino..."
bind:value={searchTo}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.equivalencies.items}
columns={createColumns(handleSuccess)}
pageCount={data.equivalencies.pages}
totalItems={data.equivalencies.total}
/>
</div>
<DataTable
data={data.equivalencies.items}
columns={createCatalogColumns({
onInsertItems: handleOpenInsertItems,
onEdit: handleOpenEditCatalog,
onSuccess: handleSuccess
})}
pageCount={data.equivalencies.pages}
totalItems={data.equivalencies.total}
/>
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleSuccess} />
<DataEquivalenciesDialog
bind:open={dataDialogOpen}
equivalency={selectedEquivalency}
mode={dataMode}
onSuccess={handleSuccess}
/>
</div>