diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py index e07f048e..0735b6e3 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py @@ -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} diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py index 5455085e..f408df88 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py @@ -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") diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py index c55fd860..f8d57080 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py @@ -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"} diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py index 31ce3584..15921208 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py @@ -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 diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index d038f4ab..d3ffc998 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -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, }, ) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index fc56fcdf..51be78a8 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -76,7 +76,8 @@ "goods": { "title": "Goods", "classes": "Classes", - "parts": "Parts" + "parts": "Parts", + "fda_codes": "FDA Codes" }, "pedimentos": { "title": "Pedimentos", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 413726cd..3a253424 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -76,7 +76,8 @@ "goods": { "title": "Mercancías", "classes": "Clases", - "parts": "Partes" + "parts": "Partes", + "fda_codes": "Códigos F.D.A." }, "pedimentos": { "title": "Pedimentos", diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 5ab973b2..b386422f 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -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", } ], }, diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/+server.ts new file mode 100644 index 00000000..5b3df145 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/+server.ts @@ -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 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts new file mode 100644 index 00000000..1c904105 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts @@ -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 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts new file mode 100644 index 00000000..21e9777b --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts @@ -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 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts new file mode 100644 index 00000000..b2944e68 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts @@ -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 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts new file mode 100644 index 00000000..c9ed9e69 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts @@ -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 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts new file mode 100644 index 00000000..e80c4d6c --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts @@ -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 }); + } +}; diff --git a/frontend/src/routes/dashboard/goods/fda-codes/+page.svelte b/frontend/src/routes/dashboard/goods/fda-codes/+page.svelte new file mode 100644 index 00000000..831668c8 --- /dev/null +++ b/frontend/src/routes/dashboard/goods/fda-codes/+page.svelte @@ -0,0 +1,469 @@ + + +
+ Gestión de códigos FDA para mercancías / FDA goods code management +
++ {filteredFdaList.length} de {fdaList.length} registros +
+Cargando registros...
+Por favor espere
+No se encontraron resultados
+Intenta con otra búsqueda
+No hay registros FDA
++ No se encontraron registros para esta compañía +
++ {fda.description || '-'} +
+ {#if fda.requirements} ++ {fda.requirements} +
+ {/if} ++ Gestión de códigos FDA para mercancías / FDA goods code management +
+(Description)
+(Product Code)
+(Cargo Storage Status)
+(Requirement)
+(Manufacture)
+(Country of Production)
+(Warehouse Code)
+(ATL Call)
+