Merge pull request 'fixed/logo_empresa' (#70) from fixed/logo_empresa into development

Reviewed-on: ADUANASOFT/anexo76#70
This commit is contained in:
2026-01-22 23:30:06 +00:00
13 changed files with 404 additions and 355 deletions

View File

@@ -61,7 +61,7 @@ class Company(Base, TimestampMixin):
# Configuración básica
logo: Mapped[Optional[str]] = mapped_column(String(255))
has_express_line: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
has_express_line: Mapped[Optional[str]] = mapped_column(String(2), default="N")
order_format_type: Mapped[Optional[str]] = mapped_column(String(19))
is_service_company: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
client_name: Mapped[Optional[str]] = mapped_column(String(300))
@@ -78,6 +78,7 @@ class Company(Base, TimestampMixin):
parts_replacement: Mapped[Optional[int]] = mapped_column(SmallInteger)
activate_facmexame: Mapped[Optional[int]] = mapped_column(SmallInteger)
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
international_firm: Mapped[Optional[int]] = mapped_column(SmallInteger)
# Configuraciones simples

View File

@@ -50,7 +50,8 @@ async def create_company(
)
service = CompanyService(db)
return service.create_company_manually(data, tenant_id=tenant_id)
new_company = service.create_company_manually(data, tenant_id=tenant_id)
return CompanyResponseDTO.model_validate(service.flatten_company_dto(new_company))
@router.get(
@@ -94,7 +95,10 @@ async def list_companies(
total_pages = (total + page_size - 1) // page_size
return {
"items": [CompanyResponseDTO.model_validate(item) for item in items],
"items": [
CompanyResponseDTO.model_validate(service.flatten_company_dto(item))
for item in items
],
"total": total,
"page": page,
"page_size": page_size,
@@ -122,135 +126,12 @@ async def get_my_companies(
service = CompanyService(db)
companies = service.get_companies_by_tenant(tenant_id)
return [CompanyResponseDTO.model_validate(company) for company in companies]
@router.get(
"/status/exists",
response_model=dict,
summary="Check if company exists for tenant",
)
async def check_company_exists(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Check if a company exists for the current tenant"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
service = CompanyService(db)
exists = service.exists_company(tenant_id)
return {"exists": exists}
@router.get(
"/info/basic/{company_id}",
response_model=dict,
summary="Get basic company info",
)
async def get_basic_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get basic information about a company"""
tenant_id = current_user.get("tenant_id")
company_id_from_user = current_user.get("company_id")
# Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return {
"id": company.id,
"name": company.name,
"rfc": company.rfc,
"program": company.program,
}
@router.get(
"/info/responsible/{company_id}",
response_model=dict,
summary="Get responsible person info",
)
async def get_responsible_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get responsible person information for a company"""
tenant_id = current_user.get("tenant_id")
company_id_from_user = current_user.get("company_id")
# Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return {
"responsible": company.responsible,
"responsible_name": company.responsible_name,
"responsible_last_name": company.responsible_last_name,
"responsible_mother_last_name": company.responsible_mother_last_name,
"responsible_rfc": company.responsible_rfc,
"position": company.position,
}
@router.get(
"/info/program/{company_id}",
response_model=dict,
summary="Get program information",
)
async def get_program_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get program information for a company"""
tenant_id = current_user.get("tenant_id")
company_id_from_user = current_user.get("company_id")
# Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return {
"program": company.program,
"program_number": company.program_number,
"prosec": company.prosec,
"prosec_authorization": company.prosec_authorization,
}
return [
CompanyResponseDTO.model_validate(service.flatten_company_dto(company))
for company in companies
]
# ... existing code ...
@router.get(
"/{company_id}",
@@ -270,6 +151,7 @@ async def get_company(
detail="Tenant ID not found in user data",
)
service = CompanyService(db)
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
if not company:
raise HTTPException(
@@ -277,7 +159,7 @@ async def get_company(
detail="Company not found",
)
return CompanyResponseDTO.model_validate(company)
return CompanyResponseDTO.model_validate(service.flatten_company_dto(company))
@router.put(
@@ -299,81 +181,28 @@ async def update_company(
detail="Tenant ID not found in user data",
)
updated_company = CompanyService.update(db, company_id, tenant_id, 0, data)
service = CompanyService(db)
updated_company = service.update(db, company_id, tenant_id, 0, data)
if not updated_company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return CompanyResponseDTO.model_validate(updated_company)
return CompanyResponseDTO.model_validate(service.flatten_company_dto(updated_company))
return CompanyResponseDTO.model_validate(updated_company)
@router.post(
"/{company_id}/upload-logo",
response_model=dict,
summary="Upload company logo",
)
async def upload_company_logo(
company_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Upload logo for a company"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
# 1. Verify company exists
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
# 2. Define upload path
# Use a persistent path: 'app_data/logos/{company_id}'
upload_dir = Path(f"app_data/logos/{company_id}")
upload_dir.mkdir(parents=True, exist_ok=True)
# 3. Save file
# Preserve original filename
filename = file.filename or "logo.png"
file_path = upload_dir / filename
try:
# Check if file exists and remove it to avoid accumulation if needed,
# or just overwrite (shutil.copyfileobj overwrites)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Could not save file: {e}",
)
# 4. Returns the absolute path keys
abs_path = str(file_path.absolute())
return {"path": abs_path}
@router.get(
"/{company_id}/logo/image",
summary="Get company logo image",
)
@router.get(
"/{company_id}/logo/image",
summary="Get company logo image",
)
async def get_company_logo_image(
company_id: int,
db: Session = Depends(get_core_db),
@@ -401,6 +230,13 @@ async def get_company_logo_image(
raise HTTPException(status_code=404, detail="Logo file not found on server")
return FileResponse(file_path)
@router.delete(
"/{company_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a company",
)
async def delete_company(
company_id: int,
db: Session = Depends(get_core_db),
@@ -497,7 +333,8 @@ async def upload_company_logo(
# Actualizar la empresa con la ruta del logo
update_data = CompanyUpdateDTO(logo=file_path)
updated_company = CompanyService.update(db, company_id, tenant_id, 0, update_data)
service = CompanyService(db)
updated_company = service.update(db, company_id, tenant_id, 0, update_data)
return {
"message": "Logo uploaded successfully",

View File

@@ -97,8 +97,139 @@ class CompanyService:
logger.error(f"Error creating company: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating company")
@staticmethod
# ==================== HELPERS FOR FIELD MAPPING ====================
def _extract_company_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Extrae campos que pertenecen a la tabla Company principal"""
company_fields = [
"name", "rfc", "curp", "main_activity", "program", "program_number",
"prosec", "prosec_authorization", "sector1", "sector2", "sector3",
"manufacturer_id", "broker_company", "responsible", "responsible_name",
"responsible_last_name", "responsible_mother_last_name", "responsible_rfc",
"position", "logo", "has_express_line", "order_format_type",
"is_service_company", "client_name", "subassembly_mode", "previous_code",
"active_labels", "active_fractions", "activate_caat", "trans_interface",
"american_costs", "scaf_readonly", "parts_replacement", "activate_facmexame",
"part_reference", "international_firm", "ftp_key", "sifra_path",
"version_type", "sql_language", "balance_operation_mode", "inter_db_name"
]
return {k: v for k, v in data.items() if k in company_fields}
def _extract_certification_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Extrae campos que pertenecen a CompanyCertification"""
cert_fields = [
"is_certified_company", "certified_company_registration",
"certified_company_start_date", "certified_company_end_date",
"annex31_certification_date", "annex31_certification_number",
"annex31_modality", "annex31_company_type", "annex31_renewal_date",
"annex31_final_certification_date", "is_oea_company", "ctpat_svi",
"trusted_exporter_number", "neec_company"
]
return {k: v for k, v in data.items() if k in cert_fields}
def _extract_prevalidator_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Extrae campos que pertenecen a CompanyPrevalidator"""
# Note: 'prevalidator_key' in DTO maps to 'key' in model
fields = {}
if "prevalidator_key" in data:
fields["key"] = data["prevalidator_key"]
# Add other fields if present in DTO in the future
return fields
def flatten_company_dto(self, company: Company) -> Dict[str, Any]:
"""Flattens Company and its submodels into a single dict for DTO validation"""
# 1. Base Company fields
result = {
k: getattr(company, k)
for k in company.__mapper__.c.keys()
}
# Explicitly ensure logo is present (defensive programming)
if hasattr(company, 'logo'):
result['logo'] = company.logo
# Convert has_express_line from String "S"/"N" to Boolean
if hasattr(company, 'has_express_line'):
val = getattr(company, 'has_express_line', "N")
result['has_express_line'] = (val == "S")
# 2. Certification fields
if company.certification:
cert_fields = [
"is_certified_company", "certified_company_registration",
"certified_company_start_date", "certified_company_end_date",
"annex31_certification_date", "annex31_certification_number",
"annex31_modality", "annex31_company_type", "annex31_renewal_date",
"annex31_final_certification_date", "is_oea_company", "ctpat_svi",
"trusted_exporter_number", "neec_company"
]
for field in cert_fields:
val = getattr(company.certification, field, None)
if val is not None:
result[field] = val
# 3. Prevalidator fields
if company.prevalidator:
if company.prevalidator.key:
result["prevalidator_key"] = company.prevalidator.key
return result
# ==================== CRUD METHODS ====================
def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company:
from .submodels.certification import CompanyCertification
from .submodels.prevalidator import CompanyPrevalidator
try:
# 1. Preparar datos
obj_data = data.model_dump(exclude_unset=True)
# Handle boolean flags for Company (Hybrid Approach)
# has_express_line is String(2), is_service_company is Boolean
if "has_express_line" in obj_data and isinstance(obj_data["has_express_line"], bool):
obj_data["has_express_line"] = "S" if obj_data["has_express_line"] else "N"
# 2. Extract fields for each model
company_data = self._extract_company_fields(obj_data)
cert_data = self._extract_certification_fields(obj_data)
preval_data = self._extract_prevalidator_fields(obj_data)
# 3. Create Company
db_company = Company(**company_data, tenant_id=tenant_id)
self.db.add(db_company)
self.db.flush() # Generate ID
# 4. Create Certification if data exists
if cert_data:
cert = CompanyCertification(**cert_data, company_id=db_company.id)
self.db.add(cert)
# 5. Create Prevalidator if data exists
if preval_data:
preval = CompanyPrevalidator(**preval_data, company_id=db_company.id)
self.db.add(preval)
# 6. Commit
self.db.commit()
self.db.refresh(db_company)
return db_company
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating company manually: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error de integridad: Es posible que esta empresa ya exista.",
)
except Exception as e:
self.db.rollback()
logger.error(f"Error creating company manually: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}")
def update(
self, # Changed to instance method to use self helper methods
db: Session,
company_id: int,
tenant_id: int,
@@ -106,23 +237,57 @@ class CompanyService:
company_data: CompanyUpdateDTO,
) -> Optional[Company]:
"""Update a company"""
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_unused)
from .submodels.certification import CompanyCertification
from .submodels.prevalidator import CompanyPrevalidator
# Use self.db if db is passed as None, or use passed db (legacy support)
session = db if db else self.db
company = self.get_by_id(session, company_id, tenant_id, company_id_unused)
if not company:
return None
# Update only provided fields
update_data = company_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
# 1. Update Company fields
company_fields = self._extract_company_fields(update_data)
# Hybrid Approach: has_express_line is String, is_service_company is Boolean
if "has_express_line" in company_fields and isinstance(company_fields["has_express_line"], bool):
company_fields["has_express_line"] = "S" if company_fields["has_express_line"] else "N"
for field, value in company_fields.items():
setattr(company, field, value)
# 2. Update Certification
cert_fields = self._extract_certification_fields(update_data)
if cert_fields:
if company.certification:
for field, value in cert_fields.items():
setattr(company.certification, field, value)
else:
new_cert = CompanyCertification(**cert_fields, company_id=company.id)
session.add(new_cert)
# 3. Update Prevalidator
preval_fields = self._extract_prevalidator_fields(update_data)
if preval_fields:
if company.prevalidator:
for field, value in preval_fields.items():
setattr(company.prevalidator, field, value)
else:
new_preval = CompanyPrevalidator(**preval_fields, company_id=company.id)
session.add(new_preval)
try:
db.commit()
db.refresh(company)
session.commit()
session.refresh(company)
return company
except Exception as e:
db.rollback()
session.rollback()
logger.error(f"Error updating company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating company")
raise HTTPException(status_code=500, detail=f"Error al actualizar la empresa: {str(e)}")
@staticmethod
def delete(
@@ -134,19 +299,56 @@ class CompanyService:
return False
try:
# Manual cascade delete for submodels to ensure order and avoid FK issues
# (Even though cascade="all, delete-orphan" is set, manual deletion is safer for strict DBs)
# 1. Delete Certification
if company.certification:
db.delete(company.certification)
# 2. Delete Prevalidator
if company.prevalidator:
db.delete(company.prevalidator)
# 3. Delete Electronic Agent
if company.electronic_agent:
db.delete(company.electronic_agent)
# 4. Delete VU
if company.ventanilla_unica:
db.delete(company.ventanilla_unica)
# 5. Delete CFDI
if company.cfdi:
db.delete(company.cfdi)
# 6. Delete Digital Certificates
for cert in company.digital_certificates:
db.delete(cert)
# 7. Delete Addresses
for addr in company.addresses:
db.delete(addr)
# Flush to execute submodel deletions first
db.flush()
db.delete(company)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting company {company_id}: {str(e)}")
# Check if it's a foreign key constraint
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar la empresa porque tiene registros relacionados (facturas, conceptos, etc.)"
)
raise HTTPException(status_code=400, detail="Error al eliminar la empresa")
# Try to get detailed error from psycopg2
detail = "No se puede eliminar la empresa porque tiene registros relacionados."
if hasattr(e, 'orig') and hasattr(e.orig, 'diag'):
if e.orig.diag.message_detail:
detail += f" Detalles: {e.orig.diag.message_detail}"
raise HTTPException(
status_code=400,
detail=detail
)
except Exception as e:
db.rollback()
logger.error(f"Error deleting company {company_id}: {str(e)}")
@@ -169,32 +371,4 @@ class CompanyService:
.filter(Company.tenant_id == tenant_id)
.first()
is not None
)
def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company:
try:
# 1. Preparar datos
obj_data = data.model_dump(exclude_unset=True)
# 2. Crear objeto SQLAlchemy
db_obj = Company(**obj_data, tenant_id=tenant_id)
# 3. Guardar
self.db.add(db_obj)
self.db.commit()
self.db.refresh(db_obj)
return db_obj
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating company manually: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error de integridad: Es posible que esta empresa ya exista.",
)
except Exception as e:
self.db.rollback()
logger.error(f"Error creating company manually: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}")
)

View File

@@ -2,7 +2,7 @@
Modelo de certificaciones de empresa
"""
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Integer, String, SmallInteger, ForeignKey
from sqlalchemy import Integer, String, SmallInteger, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
from api.v1.common.base_models import TimestampMixin

View File

@@ -17,6 +17,7 @@ ace_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(ace_router)
@@ -32,6 +33,7 @@ oma_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(oma_router)
@@ -47,6 +49,7 @@ american_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(american_router)
@@ -62,6 +65,7 @@ customs_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(customs_router)
@@ -77,6 +81,7 @@ general_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(general_router)
@@ -93,5 +98,6 @@ main_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(main_router)

15
backend/debug_mapper.py Normal file
View File

@@ -0,0 +1,15 @@
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.general_catalogs.company.service import CompanyService
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
print("Checking Company Mapper keys...")
try:
keys = Company.__mapper__.c.keys()
print(f"Keys found: {keys}")
if 'logo' in keys:
print("SUCCESS: 'logo' is in keys")
else:
print("FAILURE: 'logo' is NOT in keys")
except Exception as e:
print(f"Error: {e}")

20
backend/inspect_schema.py Normal file
View File

@@ -0,0 +1,20 @@
from sqlalchemy import create_engine, text
# Connect to DB (adjust for localhost)
DB_URL = "postgresql://postgres:postgres@localhost:5432/anexo76_core"
engine = create_engine(DB_URL)
sql = """
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = 'company' AND table_schema = 'a76'
AND column_name IN ('has_express_line', 'is_service_company');
"""
try:
with engine.connect() as conn:
result = conn.execute(text(sql))
for row in result:
print(f"Column: {row[0]}, Type: {row[1]}, Length: {row[2]}")
except Exception as e:
print(f"Error: {e}")

View File

@@ -0,0 +1,32 @@
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.general_catalogs.company.service import CompanyService
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
import os
# Connect to DB (adjust for localhost)
DB_URL = "postgresql://postgres:postgres@localhost:5432/anexo76_core"
engine = create_engine(DB_URL)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
try:
print("Querying first company...")
company = session.query(Company).first()
if company:
print(f"Company ID: {company.id}")
print(f"Direct Logo Access: '{company.logo}'")
service = CompanyService(session)
flattened = service.flatten_company_dto(company)
print(f"Flattened Logo: '{flattened.get('logo')}'")
has_logo = 'logo' in flattened
print(f"Is 'logo' key in dict?: {has_logo}")
else:
print("No companies found in DB.")
except Exception as e:
print(f"Error: {e}")
finally:
session.close()

View File

@@ -295,7 +295,7 @@ export interface UnitOfMeasureListResponse {
export async function getUnitsOfMeasure(
page: number = 1,
pageSize: number = 50,
pageSize: number = 1000,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureListResponse>> {

View File

@@ -16,15 +16,27 @@
} = $props();
let items = $state<UnitOfMeasure[]>([]);
let allItems = $state<UnitOfMeasure[]>([]); // Store full dataset
let loading = $state(false);
let searchTerm = $state("");
let page = $state(1);
let totalPages = $state(1);
let searchTimeout: NodeJS.Timeout;
// Derived state for filtering
$effect(() => {
if (!searchTerm) {
items = allItems;
} else {
const lowerTerm = searchTerm.toLowerCase();
items = allItems.filter(item =>
item.code.toLowerCase().includes(lowerTerm) ||
(item.description && item.description.toLowerCase().includes(lowerTerm)) ||
(item.description_en && item.description_en.toLowerCase().includes(lowerTerm))
);
}
});
// Cargar datos al abrir
$effect(() => {
if (open && companyStore.activeCompany) {
if (open && companyStore.activeCompany && allItems.length === 0) {
loadData();
}
});
@@ -34,17 +46,12 @@
loading = true;
try {
const filters = searchTerm ? { code: searchTerm } : {};
// Nota: Si tu backend soporta búsqueda por descripción, úsalo aquí.
// Por ahora asumo búsqueda por 'code' o 'description' según tu filtro backend.
const response = await getUnitsOfMeasure(page, 10, companyStore.activeCompany.id, {
q: searchTerm // Asumiendo que tu backend tiene un filtro genérico 'q' o usa 'code'/'description'
});
// Fetch everything once using the high limit
const response = await getUnitsOfMeasure(1, 1000, companyStore.activeCompany.id, {});
if (response.data) {
items = response.data.items;
totalPages = response.data.pages;
allItems = response.data.items;
items = allItems; // Initialize view
}
} catch (error) {
console.error("Error cargando unidades:", error);
@@ -56,32 +63,13 @@
function handleSearch(e: Event) {
const value = (e.target as HTMLInputElement).value;
searchTerm = value;
page = 1;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
loadData();
}, 500);
// No network call needed, $effect handles filtering
}
function handleSelect(item: UnitOfMeasure) {
onSelect(item);
open = false;
}
function nextPage() {
if (page < totalPages) {
page++;
loadData();
}
}
function prevPage() {
if (page > 1) {
page--;
loadData();
}
}
</script>
<Dialog.Root bind:open>
@@ -104,7 +92,7 @@
/>
</div>
<div class="rounded-md border h-[300px] overflow-auto relative">
<div class="rounded-md border h-[400px] overflow-auto relative">
{#if loading}
<div class="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
<Loader2 class="h-6 w-6 animate-spin text-primary" />
@@ -146,27 +134,9 @@
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-muted-foreground">Página {page} de {totalPages}</span>
<div class="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={page === 1 || loading}
onclick={prevPage}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages || loading}
onclick={nextPage}
>
Siguiente
</Button>
</div>
<div class="text-xs text-muted-foreground text-center">
Mostrando {items.length} registros
</div>
</div>
</Dialog.Content>

View File

@@ -10,10 +10,10 @@
const sidebar = useSidebar();
// Derivar la URL del logo
// Derivar la URL del logo usando el endpoint específico
let activeCompanyLogoUrl = $derived(
companyStore.activeCompany?.logo
? getBackendAssetUrl(companyStore.activeCompany.logo)
? getBackendAssetUrl(`v1/a76/company/${companyStore.activeCompany.id}/logo/image?t=${new Date().getTime()}`)
: null
);
@@ -89,7 +89,7 @@
<div class="flex size-6 items-center justify-center rounded-md border overflow-hidden">
{#if company.logo}
<img
src={getBackendAssetUrl(company.logo)}
src={getBackendAssetUrl(`v1/a76/company/${company.id}/logo/image`)}
alt={company.name}
class="size-full rounded object-cover"
/>

View File

@@ -144,6 +144,30 @@ class CompanyStore {
}
}
/**
* Actualiza los datos de une empresa en el store localmente
* Útil para reflejar cambios inmediatos (ej: cambio de logo) sin recargar
*/
updateCompany(id: number, data: Partial<Company>) {
// 1. Actualizar en la lista
const index = this._companies.findIndex(c => c.id === id);
if (index !== -1) {
this._companies[index] = { ...this._companies[index], ...data };
// 2. Si es la activa, actualizar también
if (this._activeCompany?.id === id) {
this._activeCompany = { ...this._activeCompany, ...data };
// Actualizar persistencia si es necesario
if (typeof window !== 'undefined') {
// Disparar evento para notificar cambios a componentes que no usan el store reactivo directo (si los hay)
window.dispatchEvent(new CustomEvent('companyChanged', {
detail: { companyId: id }
}));
}
}
}
}
/**
* Restaura la compañía activa desde localStorage
*/

View File

@@ -14,6 +14,7 @@
uploadCompanyLogo,
type Company
} from '$lib/api/dashboard/a76/general_catalogs/company';
import { companyStore } from '$lib/stores/company.svelte';
import { getBackendAssetUrl } from '$lib/utils';
import { ArrowLeft, LoaderCircle, Save, Upload, X, Building2, FileText, User, Settings } from 'lucide-svelte';
@@ -25,15 +26,18 @@
let loading = $state(false);
let uploading = $state(false);
let error = $state<string | null>(null);
let activeTab = $state('general');
let logoFile = $state<File | null>(null);
let logoPreview = $state<string | null>(null);
let currentLogo = $state<string | null>(null);
let uploadingLogo = $state(false);
let activeTab = $state('general');
// URL completa del logo derivada
let currentLogoUrl = $derived(
logoPreview || getBackendAssetUrl(currentLogo) || ''
logoPreview
? logoPreview
: (currentLogo ? getBackendAssetUrl(`v1/a76/company/${id}/logo/image?t=${new Date().getTime()}`) : '')
);
// 2. Estado Inicial (Reset)
@@ -104,7 +108,15 @@
is_service_company: item.is_service_company || false,
order_format_type: item.order_format_type || '',
ctpat_svi: item.ctpat_svi || '',
trusted_exporter_number: item.trusted_exporter_number || ''
trusted_exporter_number: item.trusted_exporter_number || '',
logo: item.logo || '',
previous_code: item.previous_code || 0,
client_name: item.client_name || '',
subassembly_mode: item.subassembly_mode || '',
broker_company: item.broker_company || '',
inter_db_name: item.inter_db_name || '',
prevalidator_key: item.prevalidator_key || '',
seventh_amendment: item.seventh_amendment || false
};
// Guardar la URL del logo actual si existe
if (item.logo) {
@@ -118,6 +130,9 @@
}
}
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
function handleLogoChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
@@ -170,6 +185,9 @@
currentLogo = response.data.logo_path;
logoFile = null;
logoPreview = null;
// Actualizar el store reactivamente
companyStore.updateCompany(companyId, { logo: response.data.logo_path });
}
} catch (e: any) {
error = `Error al subir el logo: ${e.message}`;
@@ -178,33 +196,6 @@
}
}
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
async function handleFileSelect(e: Event) {
const input = e.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const file = input.files[0];
if (!isEdit) {
alert("Primero debes guardar la empresa antes de subir un logo.");
return;
}
uploading = true;
try {
const res = await uploadCompanyLogo(Number(id), file);
if (res.data) {
formData.logo = res.data.path;
} else if (res.error) {
alert("Error al subir imagen: " + res.error);
}
} catch (err) {
alert("Error al intentar subir la imagen");
} finally {
uploading = false;
}
}
async function handleSubmit() {
error = null;
loading = true;
@@ -222,6 +213,7 @@
main_activity: clean(formData.main_activity),
program: clean(formData.program),
program_number: clean(formData.program_number),
prosec: Number(formData.prosec) || 0,
prosec_authorization: clean(formData.prosec_authorization),
responsible_name: clean(formData.responsible_name),
responsible_last_name: clean(formData.responsible_last_name),
@@ -239,7 +231,10 @@
broker_company: clean(formData.broker_company),
inter_db_name: clean(formData.inter_db_name),
prevalidator_key: clean(formData.prevalidator_key),
seventh_amendment: formData.seventh_amendment
seventh_amendment: formData.seventh_amendment,
// Ensure optional booleans are passed correctly or default to false/null if needed
has_express_line: formData.has_express_line,
is_service_company: formData.is_service_company
};
const response = isEdit
@@ -285,6 +280,8 @@
<Tabs.Root bind:value={activeTab} class="w-full">
<div class="min-h-[400px]">
<Tabs.Content value="general" class="space-y-4 pt-4">
<!-- Logo Upload Section -->
<div class="grid gap-4 p-4 border rounded-lg bg-muted/30">
<Label>Logo de la Empresa</Label>
@@ -344,36 +341,9 @@
<Label for="main_activity">Actividad Principal</Label>
<Input id="main_activity" bind:value={formData.main_activity} />
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="logo">Ruta del Logo</Label>
<div class="flex items-center gap-2">
<Input id="logo" bind:value={formData.logo} placeholder="/path/to/logo.png" />
{#if isEdit}
<div class="relative">
<Button variant="outline" size="icon" disabled={uploading}>
{#if uploading}
<LoaderCircle class="h-4 w-4 animate-spin" />
{:else}
<Upload class="h-4 w-4" />
{/if}
</Button>
<input
type="file"
accept="image/*"
class="absolute inset-0 opacity-0 cursor-pointer"
onchange={handleFileSelect}
disabled={uploading}
/>
</div>
{/if}
</div>
<p class="text-[0.8rem] text-muted-foreground">Sube una imagen para obtener su ruta local.</p>
</div>
<div class="grid gap-2">
<Label for="client_name">Nombre Cliente (Maquila)</Label>
<Input id="client_name" bind:value={formData.client_name} />
</div>
<div class="grid gap-2">
<Label for="client_name">Nombre Cliente (Maquila)</Label>
<Input id="client_name" bind:value={formData.client_name} />
</div>
</Tabs.Content>