Merge pull request 'feat: Implement CRUD operations for FDA catalog entries and their associated specifications, constituent elements, affirmation codes, and lot productions.' (#176) from feature/FDA into development
Reviewed-on: ADUANASOFT/anexo76#176
This commit is contained in:
@@ -39,6 +39,7 @@ class FDACatalogUpdate(BaseModel):
|
||||
}
|
||||
|
||||
|
||||
|
||||
class FDACatalogResponse(BaseModel):
|
||||
"""DTO para respuesta del catálogo FDA"""
|
||||
id: int
|
||||
@@ -51,9 +52,121 @@ class FDACatalogResponse(BaseModel):
|
||||
storage_status: Optional[str]
|
||||
warehouse_code: Optional[str]
|
||||
call_atl: Optional[str]
|
||||
created_at: Optional[str]
|
||||
updated_at: Optional[str]
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
|
||||
# DTOs para FDA01 - Especificaciones
|
||||
class FDASpecificationsBase(BaseModel):
|
||||
prod_code: Optional[str] = None
|
||||
commodity_desc: Optional[str] = None
|
||||
brand_name: Optional[str] = None
|
||||
disclaimer: Optional[str] = None
|
||||
pgm_code: Optional[str] = None
|
||||
proc_code: Optional[str] = None
|
||||
intnd_use_code: Optional[str] = None
|
||||
intnd_use_desc: Optional[str] = None
|
||||
temp_qual: Optional[str] = None
|
||||
temp_type: Optional[str] = None
|
||||
temp_degrees: Optional[float] = None
|
||||
temp_negative: Optional[float] = None
|
||||
temp_location: Optional[str] = None
|
||||
quantity_1: Optional[float] = None
|
||||
qty_uom_1: Optional[str] = None
|
||||
quantity_2: Optional[float] = None
|
||||
qty_uom_2: Optional[str] = None
|
||||
quantity_3: Optional[float] = None
|
||||
qty_uom_3: Optional[str] = None
|
||||
ctry_prod: Optional[str] = None
|
||||
ctry_source: Optional[str] = None
|
||||
ctry_growth: Optional[str] = None
|
||||
ctry_refusal: Optional[str] = None
|
||||
ctry_shipping: Optional[str] = None
|
||||
manuf_key: Optional[str] = None
|
||||
shipper_key: Optional[str] = None
|
||||
ult_cons_key: Optional[str] = None
|
||||
fda_imp_key: Optional[str] = None
|
||||
pn_subm_key: Optional[str] = None
|
||||
consol_key: Optional[str] = None
|
||||
producer_key: Optional[str] = None
|
||||
owner_key: Optional[str] = None
|
||||
deli_party_key: Optional[str] = None
|
||||
grower_key: Optional[str] = None
|
||||
dev_ini_imp_key: Optional[str] = None
|
||||
lacf_cont_1: Optional[str] = None
|
||||
lacf_cont_2: Optional[str] = None
|
||||
lacf_cont_3: Optional[str] = None
|
||||
pn_transmitter_key: Optional[str] = None
|
||||
|
||||
class FDASpecificationsCreate(FDASpecificationsBase):
|
||||
pass
|
||||
|
||||
class FDASpecificationsUpdate(FDASpecificationsBase):
|
||||
pass
|
||||
|
||||
class FDASpecificationsResponse(FDASpecificationsBase):
|
||||
id: int
|
||||
fda_catalog_id: int
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# DTOs para FDA04 - Elementos Constitutivos
|
||||
class FDAConstituentElementsBase(BaseModel):
|
||||
line: int
|
||||
ele_name: str
|
||||
ele_qty: Optional[float] = None
|
||||
ele_qty_uom: Optional[str] = None
|
||||
ele_pctg: Optional[float] = None
|
||||
|
||||
class FDAConstituentElementsCreate(FDAConstituentElementsBase):
|
||||
pass
|
||||
|
||||
class FDAConstituentElementsUpdate(FDAConstituentElementsBase):
|
||||
line: Optional[int] = None
|
||||
ele_name: Optional[str] = None
|
||||
|
||||
class FDAConstituentElementsResponse(FDAConstituentElementsBase):
|
||||
id: int
|
||||
fda_catalog_id: int
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# DTOs para FDA23 - Códigos de Afirmación
|
||||
class FDAAffirmationCodesBase(BaseModel):
|
||||
line: int
|
||||
aoc_code: str
|
||||
aoc_qual: Optional[str] = None
|
||||
|
||||
class FDAAffirmationCodesCreate(FDAAffirmationCodesBase):
|
||||
pass
|
||||
|
||||
class FDAAffirmationCodesUpdate(FDAAffirmationCodesBase):
|
||||
line: Optional[int] = None
|
||||
aoc_code: Optional[str] = None
|
||||
|
||||
class FDAAffirmationCodesResponse(FDAAffirmationCodesBase):
|
||||
id: int
|
||||
fda_catalog_id: int
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# DTOs para FDA25 - Lotes de Producción
|
||||
class FDALotProductionBase(BaseModel):
|
||||
line: int
|
||||
lot_number: str
|
||||
production_start_date: Optional[str] = None
|
||||
production_end_date: Optional[str] = None
|
||||
|
||||
class FDALotProductionCreate(FDALotProductionBase):
|
||||
pass
|
||||
|
||||
class FDALotProductionUpdate(FDALotProductionBase):
|
||||
line: Optional[int] = None
|
||||
lot_number: Optional[str] = None
|
||||
|
||||
class FDALotProductionResponse(FDALotProductionBase):
|
||||
id: int
|
||||
fda_catalog_id: int
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Modelo para el catálogo de claves FDA
|
||||
"""
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Integer, UniqueConstraint, PrimaryKeyConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, Numeric, Date, ForeignKey, UniqueConstraint, PrimaryKeyConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
@@ -27,3 +27,124 @@ class FDACatalog(Base, TenantScopedMixin, TimestampMixin):
|
||||
storage_status: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
warehouse_code: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
call_atl: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
# Relationships
|
||||
specifications: Mapped[Optional["FDASpecifications"]] = relationship("FDASpecifications", back_populates="fda_catalog", cascade="all, delete-orphan", uselist=False)
|
||||
constituent_elements: Mapped[List["FDAConstituentElements"]] = relationship("FDAConstituentElements", back_populates="fda_catalog", cascade="all, delete-orphan")
|
||||
affirmation_codes: Mapped[List["FDAAffirmationCodes"]] = relationship("FDAAffirmationCodes", back_populates="fda_catalog", cascade="all, delete-orphan")
|
||||
lot_productions: Mapped[List["FDALotProduction"]] = relationship("FDALotProduction", back_populates="fda_catalog", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class FDASpecifications(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""FDA01 - Especificaciones del Producto FDA"""
|
||||
__tablename__ = "fda_specifications"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="fda_specifications_pkey"),
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
prod_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
commodity_desc: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
brand_name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
disclaimer: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
pgm_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
proc_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
intnd_use_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
intnd_use_desc: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
temp_qual: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
temp_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
temp_degrees: Mapped[Optional[float]] = mapped_column(Numeric(10, 2))
|
||||
temp_negative: Mapped[Optional[float]] = mapped_column(Numeric(10, 2))
|
||||
temp_location: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
|
||||
quantity_1: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||
qty_uom_1: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
quantity_2: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||
qty_uom_2: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
quantity_3: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||
qty_uom_3: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
ctry_prod: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
ctry_source: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
ctry_growth: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
ctry_refusal: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
ctry_shipping: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
manuf_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
shipper_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
ult_cons_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
fda_imp_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
pn_subm_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
consol_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
producer_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
owner_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
deli_party_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
grower_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
dev_ini_imp_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
lacf_cont_1: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
lacf_cont_2: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
lacf_cont_3: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
pn_transmitter_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="specifications")
|
||||
|
||||
|
||||
class FDAConstituentElements(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""FDA04 - Constituent Elements"""
|
||||
__tablename__ = "fda_constituent_elements"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="fda_constituent_elements_pkey"),
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
line: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
ele_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
ele_qty: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||
ele_qty_uom: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
ele_pctg: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||
|
||||
fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="constituent_elements")
|
||||
|
||||
|
||||
class FDAAffirmationCodes(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""FDA23 - Affirmation of Compliance"""
|
||||
__tablename__ = "fda_affirmation_codes"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="fda_affirmation_codes_pkey"),
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
line: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
aoc_code: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
aoc_qual: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
|
||||
fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="affirmation_codes")
|
||||
|
||||
|
||||
class FDALotProduction(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""FDA25 - Lot and Production Dates"""
|
||||
__tablename__ = "fda_lot_production"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="fda_lot_production_pkey"),
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
line: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
lot_number: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
production_start_date: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
production_end_date: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="lot_productions")
|
||||
|
||||
@@ -67,3 +67,204 @@ async def get_fda_catalog(
|
||||
"warehouse_code": entry.warehouse_code,
|
||||
"call_atl": entry.call_atl
|
||||
}
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogCreate, FDACatalogUpdate
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
@router.post("/", response_model=FDACatalogResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_fda_catalog(
|
||||
data: FDACatalogCreate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Crear nueva entrada en el catálogo FDA"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
try:
|
||||
return FDACatalogService.create(db, tenant_id, company_id, data)
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail="La Clave FDA ya existe o hay un conflicto de datos.")
|
||||
|
||||
|
||||
@router.put("/{id}", response_model=FDACatalogResponse)
|
||||
async def update_fda_catalog(
|
||||
id: int,
|
||||
data: FDACatalogUpdate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Actualizar entrada del catálogo FDA"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
try:
|
||||
entry = FDACatalogService.update(db, tenant_id, company_id, id, data)
|
||||
if not entry:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada")
|
||||
return entry
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail="Error de integridad de datos al actualizar la Clave FDA.")
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_fda_catalog(
|
||||
id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Eliminar entrada del catálogo FDA"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
success = FDACatalogService.delete(db, tenant_id, company_id, id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada")
|
||||
return {"message": "Eliminado correctamente"}
|
||||
|
||||
from typing import List
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.service import FDADetailsService
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import (
|
||||
FDASpecificationsCreate, FDASpecificationsResponse,
|
||||
FDAConstituentElementsCreate, FDAConstituentElementsUpdate, FDAConstituentElementsResponse,
|
||||
FDAAffirmationCodesCreate, FDAAffirmationCodesUpdate, FDAAffirmationCodesResponse,
|
||||
FDALotProductionCreate, FDALotProductionUpdate, FDALotProductionResponse
|
||||
)
|
||||
|
||||
# === FDA01: Especificaciones del Producto ===
|
||||
@router.get("/{id}/specifications", response_model=Optional[FDASpecificationsResponse])
|
||||
async def get_fda_specifications(
|
||||
id: int,
|
||||
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)
|
||||
catalog_entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
|
||||
if not catalog_entry:
|
||||
raise HTTPException(status_code=404, detail="Catálogo FDA no encontrado")
|
||||
|
||||
return FDADetailsService.get_specifications(db, tenant_id, company_id, id)
|
||||
|
||||
@router.put("/{id}/specifications", response_model=FDASpecificationsResponse)
|
||||
async def save_fda_specifications(
|
||||
id: int,
|
||||
data: FDASpecificationsCreate,
|
||||
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)
|
||||
catalog_entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
|
||||
if not catalog_entry:
|
||||
raise HTTPException(status_code=404, detail="Catálogo FDA no encontrado")
|
||||
|
||||
return FDADetailsService.save_specifications(db, tenant_id, company_id, id, data.model_dump(exclude_unset=True))
|
||||
|
||||
# === FDA04: Elementos Constitutivos ===
|
||||
@router.get("/{id}/constituent-elements", response_model=List[FDAConstituentElementsResponse])
|
||||
async def list_fda_constituent_elements(
|
||||
id: int,
|
||||
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)
|
||||
return FDADetailsService.get_constituent_elements(db, tenant_id, company_id, id)
|
||||
|
||||
@router.post("/{id}/constituent-elements", response_model=FDAConstituentElementsResponse)
|
||||
async def create_fda_constituent_element(
|
||||
id: int,
|
||||
data: FDAConstituentElementsCreate,
|
||||
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)
|
||||
return FDADetailsService.create_constituent_element(db, tenant_id, company_id, id, data.model_dump())
|
||||
|
||||
@router.delete("/{id}/constituent-elements/{element_id}")
|
||||
async def delete_fda_constituent_element(
|
||||
id: int,
|
||||
element_id: int,
|
||||
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)
|
||||
success = FDADetailsService.delete_constituent_element(db, tenant_id, company_id, element_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Elemento no encontrado")
|
||||
return {"message": "Eliminado correctamente"}
|
||||
|
||||
# === FDA23: Códigos de Afirmación ===
|
||||
@router.get("/{id}/affirmation-codes", response_model=List[FDAAffirmationCodesResponse])
|
||||
async def list_fda_affirmation_codes(
|
||||
id: int,
|
||||
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)
|
||||
return FDADetailsService.get_affirmation_codes(db, tenant_id, company_id, id)
|
||||
|
||||
@router.post("/{id}/affirmation-codes", response_model=FDAAffirmationCodesResponse)
|
||||
async def create_fda_affirmation_code(
|
||||
id: int,
|
||||
data: FDAAffirmationCodesCreate,
|
||||
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)
|
||||
return FDADetailsService.create_affirmation_code(db, tenant_id, company_id, id, data.model_dump())
|
||||
|
||||
@router.delete("/{id}/affirmation-codes/{code_id}")
|
||||
async def delete_fda_affirmation_code(
|
||||
id: int,
|
||||
code_id: int,
|
||||
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)
|
||||
success = FDADetailsService.delete_affirmation_code(db, tenant_id, company_id, code_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Código no encontrado")
|
||||
return {"message": "Eliminado correctamente"}
|
||||
|
||||
# === FDA25: Lotes de Producción ===
|
||||
@router.get("/{id}/lot-productions", response_model=List[FDALotProductionResponse])
|
||||
async def list_fda_lot_productions(
|
||||
id: int,
|
||||
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)
|
||||
return FDADetailsService.get_lot_productions(db, tenant_id, company_id, id)
|
||||
|
||||
@router.post("/{id}/lot-productions", response_model=FDALotProductionResponse)
|
||||
async def create_fda_lot_production(
|
||||
id: int,
|
||||
data: FDALotProductionCreate,
|
||||
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)
|
||||
return FDADetailsService.create_lot_production(db, tenant_id, company_id, id, data.model_dump())
|
||||
|
||||
@router.delete("/{id}/lot-productions/{lot_id}")
|
||||
async def delete_fda_lot_production(
|
||||
id: int,
|
||||
lot_id: int,
|
||||
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)
|
||||
success = FDADetailsService.delete_lot_production(db, tenant_id, company_id, lot_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Lote no encontrado")
|
||||
return {"message": "Eliminado correctamente"}
|
||||
|
||||
@@ -106,3 +106,162 @@ class FDACatalogService:
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
class FDADetailsService:
|
||||
"""Servicio CRUD para los detalles adicionales de FDA (Especificaciones, Lotes, etc)"""
|
||||
|
||||
# FDA01 - Especificaciones
|
||||
@staticmethod
|
||||
def get_specifications(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDASpecifications
|
||||
return db.execute(
|
||||
select(FDASpecifications).where(
|
||||
(FDASpecifications.fda_catalog_id == fda_catalog_id) &
|
||||
(FDASpecifications.tenant_id == tenant_id) &
|
||||
(FDASpecifications.company_id == company_id)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
def save_specifications(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDASpecifications
|
||||
existing = FDADetailsService.get_specifications(db, tenant_id, company_id, fda_catalog_id)
|
||||
if existing:
|
||||
for field, value in data.items():
|
||||
setattr(existing, field, value)
|
||||
entry = existing
|
||||
else:
|
||||
entry = FDASpecifications(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
fda_catalog_id=fda_catalog_id,
|
||||
**data
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
# FDA04 - Elementos Constitutivos
|
||||
@staticmethod
|
||||
def get_constituent_elements(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAConstituentElements
|
||||
items = db.execute(
|
||||
select(FDAConstituentElements).where(
|
||||
(FDAConstituentElements.fda_catalog_id == fda_catalog_id) &
|
||||
(FDAConstituentElements.tenant_id == tenant_id) &
|
||||
(FDAConstituentElements.company_id == company_id)
|
||||
).order_by(FDAConstituentElements.line.asc())
|
||||
).scalars().all()
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def create_constituent_element(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAConstituentElements
|
||||
entry = FDAConstituentElements(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
fda_catalog_id=fda_catalog_id,
|
||||
**data
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def delete_constituent_element(db: Session, tenant_id: int, company_id: int, element_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAConstituentElements
|
||||
entry = db.execute(select(FDAConstituentElements).where(
|
||||
(FDAConstituentElements.id == element_id) &
|
||||
(FDAConstituentElements.tenant_id == tenant_id) &
|
||||
(FDAConstituentElements.company_id == company_id)
|
||||
)).scalar_one_or_none()
|
||||
if entry:
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
# FDA23 - Códigos de Afirmación
|
||||
@staticmethod
|
||||
def get_affirmation_codes(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAAffirmationCodes
|
||||
items = db.execute(
|
||||
select(FDAAffirmationCodes).where(
|
||||
(FDAAffirmationCodes.fda_catalog_id == fda_catalog_id) &
|
||||
(FDAAffirmationCodes.tenant_id == tenant_id) &
|
||||
(FDAAffirmationCodes.company_id == company_id)
|
||||
).order_by(FDAAffirmationCodes.line.asc())
|
||||
).scalars().all()
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def create_affirmation_code(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAAffirmationCodes
|
||||
entry = FDAAffirmationCodes(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
fda_catalog_id=fda_catalog_id,
|
||||
**data
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def delete_affirmation_code(db: Session, tenant_id: int, company_id: int, code_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAAffirmationCodes
|
||||
entry = db.execute(select(FDAAffirmationCodes).where(
|
||||
(FDAAffirmationCodes.id == code_id) &
|
||||
(FDAAffirmationCodes.tenant_id == tenant_id) &
|
||||
(FDAAffirmationCodes.company_id == company_id)
|
||||
)).scalar_one_or_none()
|
||||
if entry:
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
# FDA25 - Lotes de Producción
|
||||
@staticmethod
|
||||
def get_lot_productions(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDALotProduction
|
||||
items = db.execute(
|
||||
select(FDALotProduction).where(
|
||||
(FDALotProduction.fda_catalog_id == fda_catalog_id) &
|
||||
(FDALotProduction.tenant_id == tenant_id) &
|
||||
(FDALotProduction.company_id == company_id)
|
||||
).order_by(FDALotProduction.line.asc())
|
||||
).scalars().all()
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def create_lot_production(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDALotProduction
|
||||
entry = FDALotProduction(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
fda_catalog_id=fda_catalog_id,
|
||||
**data
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def delete_lot_production(db: Session, tenant_id: int, company_id: int, lot_id: int):
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDALotProduction
|
||||
entry = db.execute(select(FDALotProduction).where(
|
||||
(FDALotProduction.id == lot_id) &
|
||||
(FDALotProduction.tenant_id == tenant_id) &
|
||||
(FDALotProduction.company_id == company_id)
|
||||
)).scalar_one_or_none()
|
||||
if entry:
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -135,7 +135,7 @@ async def sqlalchemy_error_handler(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={
|
||||
"error": "DATABASE_ERROR",
|
||||
"message": "Error en la operación de base de datos",
|
||||
"message": f"Error en la operación de base de datos: {str(exc)}",
|
||||
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"goods": {
|
||||
"title": "Goods",
|
||||
"classes": "Classes",
|
||||
"parts": "Parts"
|
||||
"parts": "Parts",
|
||||
"fda_codes": "FDA Codes"
|
||||
},
|
||||
"pedimentos": {
|
||||
"title": "Pedimentos",
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"goods": {
|
||||
"title": "Mercancías",
|
||||
"classes": "Clases",
|
||||
"parts": "Partes"
|
||||
"parts": "Partes",
|
||||
"fda_codes": "Códigos F.D.A."
|
||||
},
|
||||
"pedimentos": {
|
||||
"title": "Pedimentos",
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Ship,
|
||||
Truck,
|
||||
Users,
|
||||
} from 'lucide-svelte';
|
||||
} from '@lucide/svelte';
|
||||
import * as m from "$lib/paraglide/messages.js";
|
||||
import { Title } from '../ui/alert';
|
||||
|
||||
@@ -366,6 +366,10 @@ export function getSidebarData(): SidebarData {
|
||||
{
|
||||
title: m["sidebar.goods.parts"](),
|
||||
url: "/dashboard/goods/parts",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.goods.fda_codes"](),
|
||||
url: "/dashboard/goods/fda-codes",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
283
frontend/src/routes/api-sveltekit/fda-catalog/+server.ts
Normal file
283
frontend/src/routes/api-sveltekit/fda-catalog/+server.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Helper to log to a file for debugging
|
||||
const debugLog = (msg: string) => {
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
fs.appendFileSync('/tmp/fda_proxy_debug.log', `[${timestamp}] ${msg}\n`);
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
// Precise cleaners to ensure only business data is sent to backend
|
||||
const cleanFDA01 = (d: any) => {
|
||||
if (!d) return null;
|
||||
return {
|
||||
prod_code: d.prod_code || null,
|
||||
commodity_desc: d.commodity_desc || null,
|
||||
brand_name: d.brand_name || null,
|
||||
disclaimer: d.disclaimer || null,
|
||||
pgm_code: d.pgm_code || null,
|
||||
proc_code: d.proc_code || null,
|
||||
intnd_use_code: d.intnd_use_code || null,
|
||||
intnd_use_desc: d.intnd_use_desc || null,
|
||||
temp_qual: d.temp_qual || null,
|
||||
temp_type: d.temp_type || null,
|
||||
temp_degrees: d.temp_degrees || null,
|
||||
temp_negative: d.temp_negative || null,
|
||||
temp_location: d.temp_location || null,
|
||||
quantity_1: d.quantity_1 || null,
|
||||
qty_uom_1: d.qty_uom_1 || null,
|
||||
quantity_2: d.quantity_2 || null,
|
||||
qty_uom_2: d.qty_uom_2 || null,
|
||||
quantity_3: d.quantity_3 || null,
|
||||
qty_uom_3: d.qty_uom_3 || null,
|
||||
ctry_prod: d.ctry_prod || null,
|
||||
ctry_source: d.ctry_source || null,
|
||||
ctry_growth: d.ctry_growth || null,
|
||||
ctry_refusal: d.ctry_refusal || null,
|
||||
ctry_shipping: d.ctry_shipping || null,
|
||||
manuf_key: d.manuf_key || null,
|
||||
shipper_key: d.shipper_key || null,
|
||||
ult_cons_key: d.ult_cons_key || null,
|
||||
fda_imp_key: d.fda_imp_key || null,
|
||||
pn_subm_key: d.pn_subm_key || null,
|
||||
consol_key: d.consol_key || null,
|
||||
producer_key: d.producer_key || null,
|
||||
owner_key: d.owner_key || null,
|
||||
deli_party_key: d.deli_party_key || null,
|
||||
grower_key: d.grower_key || null,
|
||||
dev_ini_imp_key: d.dev_ini_imp_key || null,
|
||||
lacf_cont_1: d.lacf_cont_1 || null,
|
||||
lacf_cont_2: d.lacf_cont_2 || null,
|
||||
lacf_cont_3: d.lacf_cont_3 || null,
|
||||
pn_transmitter_key: d.pn_transmitter_key || null
|
||||
};
|
||||
};
|
||||
|
||||
const cleanFDA23 = (d: any) => ({
|
||||
line: typeof d.line === 'number' ? d.line : (parseInt(d.line) || 1),
|
||||
aoc_code: d.aoc_code || '',
|
||||
aoc_qual: d.aoc_qual || null
|
||||
});
|
||||
|
||||
const cleanFDA04 = (d: any) => ({
|
||||
line: typeof d.line === 'number' ? d.line : (parseInt(d.line) || 1),
|
||||
ele_name: d.ele_name || '',
|
||||
ele_qty: (d.ele_qty === '' || d.ele_qty === null || d.ele_qty === undefined) ? null : parseFloat(d.ele_qty),
|
||||
ele_qty_uom: d.ele_qty_uom || null,
|
||||
ele_pctg: (d.ele_pctg === '' || d.ele_pctg === null || d.ele_pctg === undefined) ? null : parseFloat(d.ele_pctg)
|
||||
});
|
||||
|
||||
const cleanFDA25 = (d: any) => ({
|
||||
line: typeof d.line === 'number' ? d.line : (parseInt(d.line) || 1),
|
||||
lot_number: d.lot_number || '',
|
||||
production_start_date: d.production_start_date || null,
|
||||
production_end_date: d.production_end_date || null
|
||||
});
|
||||
|
||||
async function saveNestedData(id: string, companyId: string, nested: any, cookies: any, fetch: any) {
|
||||
debugLog(`saveNestedData started for id: ${id}`);
|
||||
const { fda01, fda23_codes, fda04_elements, fda25_lots } = nested;
|
||||
|
||||
// 1. FDA01 Specifications
|
||||
if (fda01) {
|
||||
debugLog(`Saving FDA01`);
|
||||
const res = await authenticatedFetch(
|
||||
`v1/a76/fda-catalog/${id}/specifications?company_id=${companyId}`,
|
||||
{ method: 'PUT', body: JSON.stringify(cleanFDA01(fda01)) },
|
||||
cookies, fetch
|
||||
);
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
debugLog(`Error FDA01: ${res.status} - ${txt}`);
|
||||
throw new Error(`Error en FDA01: ${res.status} - ${txt}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. FDA23 Affirmation Codes
|
||||
debugLog(`Processing FDA23`);
|
||||
const resExist23 = await authenticatedFetch(`v1/a76/fda-catalog/${id}/affirmation-codes?company_id=${companyId}`, { method: 'GET' }, cookies, fetch);
|
||||
if (resExist23.ok) {
|
||||
const items = await resExist23.json();
|
||||
debugLog(`Deleting ${items.length} existing FDA23 items`);
|
||||
for (const item of items) {
|
||||
await authenticatedFetch(`v1/a76/fda-catalog/${id}/affirmation-codes/${item.id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch);
|
||||
}
|
||||
}
|
||||
if (fda23_codes && Array.isArray(fda23_codes)) {
|
||||
for (const code of fda23_codes) {
|
||||
if (!code.aoc_code) continue;
|
||||
const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}/affirmation-codes?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(cleanFDA23(code)) }, cookies, fetch);
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
debugLog(`Error FDA23: ${res.status} - ${txt}`);
|
||||
throw new Error(`Error en FDA23: ${res.status} - ${txt}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. FDA04 Constituent Elements
|
||||
debugLog(`Processing FDA04`);
|
||||
const resExist04 = await authenticatedFetch(`v1/a76/fda-catalog/${id}/constituent-elements?company_id=${companyId}`, { method: 'GET' }, cookies, fetch);
|
||||
if (resExist04.ok) {
|
||||
const items = await resExist04.json();
|
||||
debugLog(`Deleting ${items.length} existing FDA04 items`);
|
||||
for (const item of items) {
|
||||
await authenticatedFetch(`v1/a76/fda-catalog/${id}/constituent-elements/${item.id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch);
|
||||
}
|
||||
}
|
||||
if (fda04_elements && Array.isArray(fda04_elements)) {
|
||||
for (const ele of fda04_elements) {
|
||||
if (!ele.ele_name) continue;
|
||||
debugLog(`Saving FDA04 item: ${ele.ele_name}`);
|
||||
const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}/constituent-elements?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(cleanFDA04(ele)) }, cookies, fetch);
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
debugLog(`Error FDA04: ${res.status} - ${txt}`);
|
||||
throw new Error(`Error en FDA04: ${res.status} - ${txt}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. FDA25 Lot Productions
|
||||
debugLog(`Processing FDA25`);
|
||||
const resExist25 = await authenticatedFetch(`v1/a76/fda-catalog/${id}/lot-productions?company_id=${companyId}`, { method: 'GET' }, cookies, fetch);
|
||||
if (resExist25.ok) {
|
||||
const items = await resExist25.json();
|
||||
debugLog(`Deleting ${items.length} existing FDA25 items`);
|
||||
for (const item of items) {
|
||||
await authenticatedFetch(`v1/a76/fda-catalog/${id}/lot-productions/${item.id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch);
|
||||
}
|
||||
}
|
||||
if (fda25_lots && Array.isArray(fda25_lots)) {
|
||||
for (const lot of fda25_lots) {
|
||||
if (!lot.lot_number) continue;
|
||||
const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}/lot-productions?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(cleanFDA25(lot)) }, cookies, fetch);
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
debugLog(`Error FDA25: ${res.status} - ${txt}`);
|
||||
throw new Error(`Error en FDA25: ${res.status} - ${txt}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
debugLog(`saveNestedData finished successfully`);
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies, fetch }) => {
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
const skip = url.searchParams.get('skip') || '0';
|
||||
const limit = url.searchParams.get('limit') || '50';
|
||||
const search = url.searchParams.get('search') || '';
|
||||
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
let apiUrl = `v1/a76/fda-catalog/?company_id=${companyId}&skip=${skip}&limit=${limit}`;
|
||||
if (search) apiUrl += `&search=${encodeURIComponent(search)}`;
|
||||
|
||||
const response = await authenticatedFetch(apiUrl, { method: 'GET' }, cookies, fetch);
|
||||
if (!response.ok) return json({ error: 'Error al cargar registros FDA' }, { status: response.status });
|
||||
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} catch (error) {
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies, fetch }) => {
|
||||
debugLog(`POST started`);
|
||||
const companyId = cookies.get('active_company_id');
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const mainRecord = {
|
||||
fda_key: body.fda_key,
|
||||
description: body.description,
|
||||
fda_code: body.fda_code || null,
|
||||
requirements: body.requirements || null,
|
||||
manufacturer_number: body.manufacturer_number || null,
|
||||
country_of_production: body.country_of_production || null,
|
||||
storage_status: body.storage_status || null,
|
||||
warehouse_code: body.warehouse_code || null,
|
||||
call_atl: body.call_atl || null
|
||||
};
|
||||
|
||||
debugLog(`Creating main record`);
|
||||
const res = await authenticatedFetch(`v1/a76/fda-catalog/?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(mainRecord) }, cookies, fetch);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
debugLog(`Error creating main record: ${res.status}`);
|
||||
return json({ error: data.detail || 'Error al crear FDA' }, { status: res.status });
|
||||
}
|
||||
|
||||
await saveNestedData(data.id.toString(), companyId, body, cookies, fetch);
|
||||
debugLog(`POST finished successfully`);
|
||||
return json(data);
|
||||
} catch (error: any) {
|
||||
debugLog(`POST Crash: ${error.message}`);
|
||||
console.error('POST Error:', error);
|
||||
return json({ error: error.message || 'Error al procesar el guardado' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ request, url, cookies, fetch }) => {
|
||||
debugLog(`PUT started`);
|
||||
const id = url.searchParams.get('id');
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
debugLog(`PUT params: id=${id}, companyId=${companyId}`);
|
||||
if (!id || !companyId) return json({ error: 'ID o compañía no proporcionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const mainRecord = {
|
||||
fda_key: body.fda_key,
|
||||
description: body.description,
|
||||
fda_code: body.fda_code || null,
|
||||
requirements: body.requirements || null,
|
||||
manufacturer_number: body.manufacturer_number || null,
|
||||
country_of_production: body.country_of_production || null,
|
||||
storage_status: body.storage_status || null,
|
||||
warehouse_code: body.warehouse_code || null,
|
||||
call_atl: body.call_atl || null
|
||||
};
|
||||
|
||||
debugLog(`Updating main record id=${id}`);
|
||||
const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}?company_id=${companyId}`, { method: 'PUT', body: JSON.stringify(mainRecord) }, cookies, fetch);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
debugLog(`Error updating main record: ${res.status}`);
|
||||
return json({ error: data.detail || 'Error al actualizar FDA' }, { status: res.status });
|
||||
}
|
||||
|
||||
await saveNestedData(id, companyId, body, cookies, fetch);
|
||||
debugLog(`PUT finished successfully`);
|
||||
return json(data);
|
||||
} catch (error: any) {
|
||||
debugLog(`PUT Crash: ${error.message}`);
|
||||
console.error('PUT Error:', error);
|
||||
return json({ error: error.message || 'Error al procesar la actualización' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ url, cookies, fetch }) => {
|
||||
const id = url.searchParams.get('id');
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
if (!id || !companyId) return json({ error: 'ID o compañía no proporcionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch);
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
return json({ error: data.detail || 'Error al eliminar FDA' }, { status: res.status });
|
||||
}
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 });
|
||||
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
const id = params.id;
|
||||
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/fda-catalog/${id}?company_id=${companyId}`,
|
||||
{ method: 'GET', cache: 'no-store' },
|
||||
cookies, fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return json({ error: 'Error al cargar registro FDA' }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} catch (error) {
|
||||
console.error('Error in fda-catalog detail API:', error);
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 });
|
||||
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
const id = params.id;
|
||||
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/fda-catalog/${id}/affirmation-codes?company_id=${companyId}`,
|
||||
{ method: 'GET', cache: 'no-store' },
|
||||
cookies, fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return json({ error: 'Error al cargar affirmation codes' }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} catch (error) {
|
||||
console.error('Error in affirmation-codes API:', error);
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 });
|
||||
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
const id = params.id;
|
||||
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/fda-catalog/${id}/constituent-elements?company_id=${companyId}`,
|
||||
{ method: 'GET', cache: 'no-store' },
|
||||
cookies, fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return json({ error: 'Error al cargar elementos constitutivos' }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} catch (error) {
|
||||
console.error('Error in constituent-elements API:', error);
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 });
|
||||
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
const id = params.id;
|
||||
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/fda-catalog/${id}/lot-productions?company_id=${companyId}`,
|
||||
{ method: 'GET', cache: 'no-store' },
|
||||
cookies, fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return json({ error: 'Error al cargar lot productions' }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} catch (error) {
|
||||
console.error('Error in lot-productions API:', error);
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 });
|
||||
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
const id = params.id;
|
||||
|
||||
if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/fda-catalog/${id}/specifications?company_id=${companyId}`,
|
||||
{ method: 'GET', cache: 'no-store' },
|
||||
cookies, fetch
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return json({ error: 'Error al cargar especificaciones' }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return json(data);
|
||||
} catch (error) {
|
||||
console.error('Error in specifications API:', error);
|
||||
return json({ error: 'Error de conexión' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
469
frontend/src/routes/dashboard/goods/fda-codes/+page.svelte
Normal file
469
frontend/src/routes/dashboard/goods/fda-codes/+page.svelte
Normal file
@@ -0,0 +1,469 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import {
|
||||
Plus,
|
||||
RefreshCw,
|
||||
FileText,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Search,
|
||||
Filter,
|
||||
Building2,
|
||||
Globe,
|
||||
Box,
|
||||
CheckSquare,
|
||||
Square,
|
||||
AlertCircle
|
||||
} from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
let fdaList = $state<any[]>([]);
|
||||
let loading = $state(false);
|
||||
let selectedRows = $state<number[]>([]);
|
||||
let mounted = $state(false);
|
||||
let searchQuery = $state('');
|
||||
let hoveredRow = $state<number | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (mounted && companyId) {
|
||||
fdaList = [];
|
||||
loadAllFdaData();
|
||||
}
|
||||
});
|
||||
|
||||
// Filtrar por búsqueda
|
||||
const filteredFdaList = $derived(
|
||||
searchQuery.trim()
|
||||
? fdaList.filter(
|
||||
(fda) =>
|
||||
fda.fda_key?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
fda.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
fda.fda_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
fda.country_of_production?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: fdaList
|
||||
);
|
||||
|
||||
async function loadAllFdaData() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api-sveltekit/fda-catalog?company_id=${companyId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
fdaList = data.items || [];
|
||||
selectedRows = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cargando lista de FDA:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
goto('/dashboard/goods/fda-codes/edit/new');
|
||||
}
|
||||
|
||||
function handleEditClick(fdaId: number) {
|
||||
goto(`/dashboard/goods/fda-codes/edit/${fdaId}`);
|
||||
}
|
||||
|
||||
async function deleteFdaCode(id: number, event?: MouseEvent) {
|
||||
event?.stopPropagation();
|
||||
if (!confirm('¿Está seguro de eliminar este código FDA?')) return;
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api-sveltekit/fda-catalog?id=${id}&company_id=${companyId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success('Registro eliminado correctamente');
|
||||
selectedRows = selectedRows.filter((r) => r !== id);
|
||||
fdaList = fdaList.filter((f) => f.id !== id);
|
||||
} else {
|
||||
const error = await res.json();
|
||||
toast.error(error.error || 'Error al eliminar');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
toast.error('Error de conexión');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRowSelection(id: number) {
|
||||
if (selectedRows.includes(id)) {
|
||||
selectedRows = selectedRows.filter((r) => r !== id);
|
||||
} else {
|
||||
selectedRows = [...selectedRows, id];
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllSelection(checked: boolean) {
|
||||
if (checked) {
|
||||
selectedRows = filteredFdaList.map((row) => row.id);
|
||||
} else {
|
||||
selectedRows = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteSelected() {
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
if (!confirm(`¿Está seguro de eliminar ${selectedRows.length} código(s) FDA?`)) return;
|
||||
|
||||
for (const id of selectedRows) {
|
||||
await deleteFdaCode(id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditSelected() {
|
||||
if (selectedRows.length !== 1) return;
|
||||
const id = selectedRows[0];
|
||||
handleEditClick(id);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedRows = [];
|
||||
}
|
||||
|
||||
// Obtener badge de estado de almacenaje
|
||||
function getStorageBadge(status: string) {
|
||||
const styles: Record<string, string> = {
|
||||
ACTIVO: 'bg-green-100 text-green-700 border-green-200',
|
||||
INACTIVO: 'bg-gray-100 text-gray-700 border-gray-200',
|
||||
PENDIENTE: 'bg-yellow-100 text-yellow-700 border-yellow-200'
|
||||
};
|
||||
return styles[status?.toUpperCase()] || 'bg-blue-100 text-blue-700 border-blue-200';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gradient-to-b from-background to-muted/20 pb-28">
|
||||
<!-- Header Principal -->
|
||||
<div class="sticky top-0 z-20 border-b bg-card/50 backdrop-blur-sm">
|
||||
<div class="mx-auto max-w-[1600px] px-4 py-6 sm:px-6">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-lg bg-primary/10 p-2">
|
||||
<FileText class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl">Códigos F.D.A.</h1>
|
||||
</div>
|
||||
<p class="pl-[52px] text-sm text-muted-foreground sm:text-base">
|
||||
Gestión de códigos FDA para mercancías / FDA goods code management
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={loadAllFdaData}
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
<span class="hidden sm:inline">Actualizar</span>
|
||||
</Button>
|
||||
<Button onclick={handleCreateClick} size="sm" class="h-10 shadow-lg shadow-primary/20">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
<span class="hidden sm:inline">Nuevo Código</span>
|
||||
<span class="sm:hidden">Nuevo</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenido Principal -->
|
||||
<div class="mx-auto max-w-[1600px] px-4 py-6 sm:px-6">
|
||||
<Card.Root class="overflow-hidden border-0 shadow-xl shadow-black/5">
|
||||
<!-- Header de Tabla con Búsqueda -->
|
||||
<div class="border-b bg-muted/30 p-4 sm:p-6">
|
||||
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-lg bg-primary/10 p-2">
|
||||
<Filter class="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Registros FDA</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{filteredFdaList.length} de {fdaList.length} registros
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full sm:w-80">
|
||||
<Search
|
||||
class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Buscar por clave, código, descripción..."
|
||||
bind:value={searchQuery}
|
||||
class="h-10 bg-background pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barra de selección -->
|
||||
{#if selectedRows.length > 0}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-top-2 mt-4 flex items-center justify-between rounded-lg border border-primary/20 bg-primary/5 p-3"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<CheckSquare class="h-5 w-5 text-primary" />
|
||||
<span class="text-sm font-medium">
|
||||
{selectedRows.length}
|
||||
{selectedRows.length === 1 ? 'registro seleccionado' : 'registros seleccionados'}
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onclick={clearSelection} class="h-8">
|
||||
Limpiar selección
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tabla -->
|
||||
<div class="overflow-x-auto">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row class="bg-muted/50 hover:bg-muted/50">
|
||||
<Table.Head class="w-12 text-center">
|
||||
<Checkbox
|
||||
checked={filteredFdaList.length > 0 &&
|
||||
selectedRows.length === filteredFdaList.length}
|
||||
indeterminate={selectedRows.length > 0 &&
|
||||
selectedRows.length < filteredFdaList.length}
|
||||
onCheckedChange={toggleAllSelection}
|
||||
aria-label="Seleccionar todos"
|
||||
/>
|
||||
</Table.Head>
|
||||
<Table.Head class="font-semibold whitespace-nowrap">Clave FDA</Table.Head>
|
||||
<Table.Head class="font-semibold">Descripción</Table.Head>
|
||||
<Table.Head class="font-semibold whitespace-nowrap">Código FDA</Table.Head>
|
||||
<Table.Head class="font-semibold whitespace-nowrap">Fabricante</Table.Head>
|
||||
<Table.Head class="font-semibold whitespace-nowrap">País</Table.Head>
|
||||
<Table.Head class="font-semibold whitespace-nowrap">Estado</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="py-16 text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="rounded-full bg-primary/10 p-4">
|
||||
<RefreshCw class="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-medium">Cargando registros...</p>
|
||||
<p class="text-sm text-muted-foreground">Por favor espere</p>
|
||||
</div>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else if filteredFdaList.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="py-16 text-center">
|
||||
{#if searchQuery}
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="rounded-full bg-muted p-4">
|
||||
<Search class="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-medium">No se encontraron resultados</p>
|
||||
<p class="text-sm text-muted-foreground">Intenta con otra búsqueda</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={() => (searchQuery = '')}>
|
||||
Limpiar búsqueda
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="rounded-full bg-muted p-4">
|
||||
<Box class="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-medium">No hay registros FDA</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
No se encontraron registros para esta compañía
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each filteredFdaList as fda, i (fda.id)}
|
||||
<Table.Row
|
||||
class={cn(
|
||||
'cursor-pointer transition-all duration-150',
|
||||
selectedRows.includes(fda.id) && 'bg-primary/5 hover:bg-primary/10',
|
||||
hoveredRow === fda.id && !selectedRows.includes(fda.id) && 'bg-muted/30',
|
||||
'hover:shadow-sm'
|
||||
)}
|
||||
onmouseenter={() => (hoveredRow = fda.id)}
|
||||
onmouseleave={() => (hoveredRow = null)}
|
||||
onclick={() => toggleRowSelection(fda.id)}
|
||||
>
|
||||
<Table.Cell class="text-center" onclick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(fda.id)}
|
||||
onCheckedChange={() => toggleRowSelection(fda.id)}
|
||||
aria-label="Seleccionar fila"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="rounded bg-primary/10 px-2 py-1 font-mono text-sm font-bold text-primary"
|
||||
>
|
||||
{fda.fda_key}
|
||||
</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="max-w-[300px]">
|
||||
<p class="truncate font-medium" title={fda.description}>
|
||||
{fda.description || '-'}
|
||||
</p>
|
||||
{#if fda.requirements}
|
||||
<p
|
||||
class="mt-0.5 truncate text-xs text-muted-foreground"
|
||||
title={fda.requirements}
|
||||
>
|
||||
{fda.requirements}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span class="font-mono text-sm">
|
||||
{fda.fda_code || '-'}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Building2 class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span class="text-sm">{fda.manufacturer_number || '-'}</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<Globe class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span class="text-sm">{fda.country_of_production || '-'}</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if fda.storage_status}
|
||||
<Badge
|
||||
variant="outline"
|
||||
class={cn('text-xs font-medium', getStorageBadge(fda.storage_status))}
|
||||
>
|
||||
{fda.storage_status}
|
||||
</Badge>
|
||||
{:else}
|
||||
<span class="text-sm text-muted-foreground">-</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Footer de Tabla -->
|
||||
{#if filteredFdaList.length > 0}
|
||||
<div class="flex items-center justify-between border-t bg-muted/30 px-4 py-3">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
Mostrando {filteredFdaList.length} registros
|
||||
</span>
|
||||
{#if searchQuery}
|
||||
<Badge variant="secondary" class="gap-2">
|
||||
Filtro: "{searchQuery}"
|
||||
<button onclick={() => (searchQuery = '')} class="hover:text-destructive"> × </button>
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Footer Fijo con Acciones -->
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-30 border-t bg-background/95 shadow-[0_-4px_20px_rgba(0,0,0,0.1)] backdrop-blur supports-[backdrop-filter]:bg-background/90"
|
||||
>
|
||||
<div class="mx-auto max-w-[1600px] px-4 py-4 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Info de selección -->
|
||||
<div class="flex items-center gap-4">
|
||||
{#if selectedRows.length > 0}
|
||||
<div class="flex items-center gap-3 rounded-full bg-primary/10 px-4 py-2">
|
||||
<CheckSquare class="h-4 w-4 text-primary" />
|
||||
<span class="text-sm font-semibold text-primary">
|
||||
{selectedRows.length} seleccionado(s)
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<AlertCircle class="h-4 w-4" />
|
||||
<span>Selecciona registros para editar o eliminar</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Botones de acción -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Button size="default" onclick={handleCreateClick} class="shadow-lg shadow-primary/20">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
<span class="hidden sm:inline">Insertar</span>
|
||||
<span class="sm:hidden">Nuevo</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
disabled={selectedRows.length !== 1}
|
||||
onclick={handleEditSelected}
|
||||
class={selectedRows.length === 1 ? 'border-primary/50' : ''}
|
||||
>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
<span class="hidden sm:inline">Editar</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
disabled={selectedRows.length === 0}
|
||||
onclick={handleDeleteSelected}
|
||||
class={selectedRows.length > 0
|
||||
? 'border-destructive/50 text-destructive hover:bg-destructive/10'
|
||||
: ''}
|
||||
>
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
<span class="hidden sm:inline">Borrar</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
1071
frontend/src/routes/dashboard/goods/fda-codes/edit/[id]/+page.svelte
Normal file
1071
frontend/src/routes/dashboard/goods/fda-codes/edit/[id]/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user