Parametros generales SCAII, con endpint y con la mayoria de pestanias hechas y funcionalaes

This commit is contained in:
2026-03-30 13:05:21 -05:00
parent d35f02563a
commit 4541629b3f
18 changed files with 2483 additions and 1750 deletions

View File

@@ -0,0 +1,40 @@
"""add_app_settings_table
Revision ID: e76_app_settings
Revises: c1a2b3d4e5f6
Create Date: 2026-03-27 16:10:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'e76_app_settings'
down_revision = 'c1a2b3d4e5f6'
branch_labels = None
depends_on = None
def upgrade():
# Create a76.app_settings table
op.create_table(
'app_settings',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=True),
sa.Column('company_id', sa.Integer(), nullable=True),
sa.Column('settings', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], ),
sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('tenant_id', 'company_id', name='uq_app_settings_tenant_company'),
schema='a76'
)
op.create_index(op.f('ix_a76_app_settings_company_id'), 'app_settings', ['company_id'], unique=False, schema='a76')
op.create_index(op.f('ix_a76_app_settings_tenant_id'), 'app_settings', ['tenant_id'], unique=False, schema='a76')
def downgrade():
op.drop_index(op.f('ix_a76_app_settings_tenant_id'), table_name='app_settings', schema='a76')
op.drop_index(op.f('ix_a76_app_settings_company_id'), table_name='app_settings', schema='a76')
op.drop_table('app_settings', schema='a76')

View File

@@ -0,0 +1 @@
# Module initialization for app_settings

View File

@@ -0,0 +1,33 @@
from typing import Optional
from sqlalchemy import Integer, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
from api.v1.common.base_models import TimestampMixin
class AppSetting(Base, TimestampMixin):
"""
Unified configuration table for Anexo 76.
Replaces 14 legacy tables using a hierarchical JSONB override system.
"""
__tablename__ = "app_settings"
__table_args__ = (
UniqueConstraint("tenant_id", "company_id", name="uq_app_settings_tenant_company"),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# Hierarchy levels (Nullable to allow Global/Tenant/Company scoping)
tenant_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("core.tenants.id"), nullable=True, index=True
)
company_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("a76.company.id"), nullable=True, index=True
)
# The actual configuration payload
settings: Mapped[dict] = mapped_column(JSONB, nullable=False, default={})
def __repr__(self):
return f"<AppSetting(id={self.id}, tenant={self.tenant_id}, company={self.company_id})>"

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
from core.database import get_core_db
from .service import AppSettingsService
from .schemas import AppSettingRequest, AppSettingResponse
router = APIRouter(prefix="/a76/app-settings", tags=["a76 / app_settings"])
import logging
import traceback
logger = logging.getLogger(__name__)
@router.get("/resolved")
def get_resolved_settings(
tenant_id: int = Query(...),
company_id: int = Query(...),
db: Session = Depends(get_core_db)
):
"""
Returns the final merged configuration for a company.
Merges Global -> Tenant -> Company levels.
"""
try:
return AppSettingsService.get_resolved_settings(db, tenant_id, company_id)
except Exception as e:
logger.error(f"RESOLVE ERROR: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.post("/upsert")
def upsert_settings(
payload: AppSettingRequest,
db: Session = Depends(get_core_db)
):
"""
Creates or updates an override for a specific level (Global, Tenant, or Company).
"""
try:
data = payload.settings.model_dump(exclude_unset=True)
return AppSettingsService.upsert_settings(
db,
payload.tenant_id,
payload.company_id,
data
)
except Exception as e:
logger.error(f"UPSERT ERROR: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.put("/upsert")
def update_settings(
payload: AppSettingRequest,
db: Session = Depends(get_core_db)
):
"""
Alias for upsert_settings.
"""
return upsert_settings(payload, db)

View File

@@ -0,0 +1,989 @@
from typing import Optional, Any, Dict
from pydantic import BaseModel, Field
from decimal import Decimal
# --- Legacy Table Domains (Tarea 1) ---
class SSisGenSettings(BaseModel):
"""
Legacy Table: SSisGen
General system parameters migrated from Clarion/WinDev.
"""
consecutivo: Optional[int] = None
dta: Optional[int] = None
dtaexpo: Optional[int] = None
subempresa: Optional[str] = None
patharch: Optional[str] = None
patharchtransmision: Optional[str] = None
pathtransexpo: Optional[str] = None
pathrespuesta: Optional[str] = None
patharchped: Optional[str] = None
patharchpedconsm: Optional[str] = None
pathgenimpotemp: Optional[str] = None
pathgenexpo: Optional[str] = None
actseguridad: Optional[int] = None
controldes: Optional[int] = None
diadesactual: Optional[int] = None
diavencimiento: Optional[int] = None
mensajevenc: Optional[int] = None
fechades: Optional[int] = None
factoriva: Optional[Decimal] = None
validasifra: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
calvalbasetcped: Optional[int] = None
calvalbasetcpedexpo: Optional[int] = None
filtrocantidad: Optional[Decimal] = None
muestraarchcodbarras: Optional[int] = None
datoshist: Optional[int] = None
tipovenro: Optional[str] = None
cantvenro: Optional[int] = None
costoplanta: Optional[Decimal] = None
firmapacking: Optional[int] = None
advertenciatm: Optional[int] = None
tomarsaldosvenc: Optional[int] = None
costoimpofijo: Optional[int] = None
valparteexiste: Optional[int] = None
valmanifusado: Optional[int] = None
temporalfechapago: Optional[int] = None
asignadiasantdesc: Optional[int] = None
diasantdesc: Optional[int] = None
deshabilitardescparte: Optional[int] = None
deshabilitardescparteing: Optional[int] = None
asignafracameparte: Optional[int] = None
validadecencant: Optional[int] = None
usartranspamedocame: Optional[int] = None
mostraradvertenciaro: Optional[int] = None
escondaamexpacking: Optional[int] = None
calcdutypacking: Optional[int] = None
parammultiples: Optional[int] = None
fraccnivelpais: Optional[int] = None
covefechaemision: Optional[int] = None
valordllstcfacturaexpo: Optional[int] = None
interfaceaaconsolidada: Optional[int] = None
interfaceaatcfpff: Optional[str] = None
incluirobscoveobsimpo: Optional[int] = None
agregarincreimpo: Optional[int] = None
componentebom: Optional[int] = None
limitesubensamble: Optional[int] = None
actpdfreportes: Optional[int] = None
patharchpdfimpo: Optional[str] = None
patharchpdfexpo: Optional[str] = None
noimprimircons: Optional[int] = None
mostraradvertenciarovalor: Optional[int] = None
partesypedimentosporcliente: Optional[str] = None
mensajesvurfc: Optional[int] = None
mostrarpackinglistingles: Optional[int] = None
omitirempaqueencodigobarras: Optional[int] = None
restringepaisimpo: Optional[int] = None
bloqueoaldesactivarnumerodeparte: Optional[int] = None
restringpaisexpo: Optional[int] = None
geninformeanexo31: Optional[str] = None
utilizarfechapagopeddeundiaanterior: Optional[int] = None
utilizarequivalenciasdeumpornumerodeparte: Optional[int] = None
utilizartitulosalternativosimpresionfactura: Optional[int] = None
usarfactorconversionpornumerodeparte: Optional[int] = None
usarvude128o256: Optional[int] = None
usartcdelafechapagopedimpoendescarga: Optional[int] = None
solicitarcontrasenaadministrador: Optional[int] = None
agregarnumeroembarque: Optional[int] = None
utilizarumdeexistenciaentransmisionvu: Optional[int] = None
utilizarcodigodebrokerdeclienteenmainx30: Optional[int] = None
hojacalculosepararincrementablesanexo3: Optional[int] = None
hojacalculodesglosefacturaanexo3: Optional[int] = None
usarvaloragregadoenfacturaamericana: Optional[int] = None
ocultarinformacionfraccion: Optional[int] = None
resaltarsaldostempconcolor: Optional[int] = None
valoragregadoenfacturamexicana: Optional[int] = None
validarsectorprosecr8: Optional[int] = None
agregarsubtotalinterfazaa: Optional[int] = None
utilizarnombregenericomainx30: Optional[int] = None
utilizartcrespectoatipoped: Optional[int] = None
tomardecimalescompletos: Optional[int] = None
incluirremesaeninterfazaawinsaai: Optional[int] = None
cambiarpesosporcostounitario: Optional[int] = None
utilizarsolopartesnaftaenco: Optional[int] = None
bloqueodeediciondefacturas: Optional[int] = None
reasignafraccionclase: Optional[int] = None
calcularcostounitarioenbaseavalortotal: Optional[int] = None
parametroauxiliar: Optional[int] = None
usarcontroldefechasdeversion: Optional[int] = None
desactivaciondemodulos: Optional[int] = None
imprimirfacturaalterna: Optional[int] = None
activarexpedienteelectronico: Optional[int] = None
activarcatalogofraccionesamericanassifra: Optional[int] = None
informacionamericanasubtotal: Optional[int] = None
mostrarprogramaimmexprosec: Optional[int] = None
costounitarioporempaquefac: Optional[int] = None
transmitirfacalterna: Optional[int] = None
class SSisGen2Settings(BaseModel):
"""
Legacy Table: SSisGen2
Extended system parameters.
"""
consecutivo: Optional[int] = None
actvaloragre: Optional[int] = None
valoragregadogen: Optional[Decimal] = None
class SSisGen3Settings(BaseModel):
"""
Legacy Table: SSisGen3
Additional name-value parameters.
"""
parametro: Optional[str] = None
valorparametro: Optional[int] = None
class SSisMexSettings(BaseModel):
"""Legacy Table: SSisMex (Mexican Purchases)"""
consecutivo: Optional[int] = None
prefijocm: Optional[str] = None
consecutivocm: Optional[str] = None
porparteclasemex: Optional[str] = None
porparteclaseame: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
flete: Optional[Decimal] = None
paisorigenmex: Optional[int] = None
numpartemex: Optional[int] = None
firmafmex: Optional[str] = None
fraccionimp: Optional[int] = None
tipofraccmex: Optional[int] = None
tasafraccmex: Optional[int] = None
umequivalentemex: Optional[int] = None
numparteame: Optional[int] = None
fraccioname: Optional[int] = None
paisorigename: Optional[int] = None
umequivalenteame: Optional[int] = None
firmafame: Optional[str] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
clavemoneda: Optional[str] = None
umauxiliarmex: Optional[int] = None
umalternamex: Optional[int] = None
transportista: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
clavebultos: Optional[str] = None
packingordencompra: Optional[int] = None
firmaelectronica: Optional[str] = None
incluirlineadelpo: Optional[int] = None
lineapersonaaa: Optional[int] = None
agregarvaenprodterminados: Optional[int] = None
ocultarfechahora: Optional[int] = None
class SSisDefSettings(BaseModel):
"""Legacy Table: SSisDef (Definitive Import)"""
consecutivo: Optional[int] = None
prefijoimpo: Optional[str] = None
consecutivoimpo: Optional[str] = None
porparteclasemex: Optional[str] = None
porparteclaseame: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
firmafmex: Optional[str] = None
firmafame: Optional[str] = None
codigobarras: Optional[int] = None
numpartemex: Optional[int] = None
numparteame: Optional[int] = None
fraccionimp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
paisorigenmex: Optional[int] = None
umequivalentemex: Optional[int] = None
umauxiliarmex: Optional[int] = None
fraccioname: Optional[int] = None
paisorigename: Optional[int] = None
umequivalenteame: Optional[int] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
clavemoneda: Optional[str] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
umalternamex: Optional[int] = None
restringeimpopt: Optional[int] = None
partecomplementariamex: Optional[int] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
metvalor: Optional[str] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
packingpedclave: Optional[int] = None
porordenporocpack: Optional[str] = None
actualizarpartepartida: Optional[int] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
clavebultos: Optional[str] = None
partecomplemexpartida: Optional[int] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
packingordencompra: Optional[int] = None
firmaelectronica: Optional[str] = None
solicitarcontrasenaadministrador: Optional[int] = None
incluirlineadelpo: Optional[int] = None
lineapersonaaa: Optional[int] = None
agregarvaenprodterminados: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
ocultarfechahora: Optional[int] = None
class SSisExpoSettings(BaseModel):
"""Legacy Table: SSisExpo (Exports)"""
tipofactura: Optional[str] = None
porparteclasemex: Optional[str] = None
porparteclaseame: Optional[str] = None
prefijoexpo: Optional[str] = None
consecutivoexpo: Optional[str] = None
pedimento: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
vendidopor: Optional[str] = None
aaduanal: Optional[str] = None
tipo: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
tipomoneda: Optional[str] = None
moneda: Optional[str] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
numpartemex: Optional[int] = None
paisorigenmex: Optional[int] = None
fraccionexp: Optional[int] = None
tasafraccmex: Optional[int] = None
costounitmex: Optional[int] = None
ordencompmex: Optional[int] = None
consfacmex: Optional[int] = None
umequivalentemex: Optional[int] = None
ocultarempaquemex: Optional[int] = None
costounitmpmex: Optional[int] = None
codigobarras: Optional[int] = None
partecomplementariamex: Optional[int] = None
valordllsmexpesos: Optional[int] = None
cantpncodbar: Optional[int] = None
dtacerocodbar: Optional[int] = None
firmafmex: Optional[str] = None
numparteame: Optional[int] = None
firmafame: Optional[str] = None
fraccioname: Optional[int] = None
paisorigename: Optional[int] = None
ordencompame: Optional[int] = None
umequivalenteame: Optional[int] = None
fdainformacioname: Optional[int] = None
paisopcame: Optional[int] = None
muestrafree: Optional[int] = None
fraccmexdes: Optional[int] = None
tipofraccmexdes: Optional[int] = None
colvalordes: Optional[int] = None
colpesodes: Optional[int] = None
descargaclase: Optional[int] = None
descargasust: Optional[int] = None
descargadef: Optional[int] = None
ventqueueact: Optional[int] = None
codigoscac: Optional[int] = None
formadesperdicio: Optional[str] = None
calvalorbasecosto: Optional[int] = None
calvabasecapt: Optional[int] = None
calpesobasedescarga: Optional[int] = None
calempaquebasecapt: Optional[int] = None
ocultcantcons: Optional[int] = None
prefijofraccmult: Optional[int] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
costamepaisparte: Optional[int] = None
basecostompototal: Optional[str] = None
umequivalentemex2: Optional[int] = None
umequivalenteame2: Optional[int] = None
totalequivmex: Optional[int] = None
totalequivame: Optional[int] = None
infoadicional: Optional[str] = None
restringeexpomp: Optional[int] = None
calvatotagremp: Optional[str] = None
activaadvpedrtv1: Optional[int] = None
asignafrac9801: Optional[int] = None
valorexcfrac9801: Optional[Decimal] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
scrapsolonoduty: Optional[int] = None
descextraamepartes: Optional[int] = None
incambioregdesc: Optional[int] = None
escambioregimen: Optional[str] = None
descnocontemcant: Optional[int] = None
metvalor: Optional[str] = None
ocultarempaqueame: Optional[int] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
pesonetodesdiffac: Optional[Decimal] = None
enviadoporvendidopor: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
clavebultos: Optional[str] = None
ocultacolva: Optional[int] = None
calccostosamebasemex: Optional[int] = None
proveedorexportador: Optional[str] = None
tipofraccmex: Optional[int] = None
partecomplemexpartida: Optional[int] = None
validacantparcandes: Optional[int] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
costosamepais: Optional[str] = None
incluirfracamebil: Optional[int] = None
solicitarpswdactual: Optional[int] = None
solicitarpswddesactual: Optional[int] = None
buscarsaldosrecientes: Optional[int] = None
packingordenventa: Optional[int] = None
imprimirordenventa: Optional[int] = None
firmaelectronica: Optional[str] = None
mpf: Optional[int] = None
valormpf: Optional[Decimal] = None
colcantporpeso: Optional[int] = None
packingpedclave: Optional[int] = None
generafacturaimd: Optional[str] = None
generafacturaimdenbasea: Optional[str] = None
solicitarcontrasenaadministrador: Optional[int] = None
incluirlineadelpo: Optional[int] = None
valorpacking1dlls: Optional[int] = None
tomarvalormexenfacame: Optional[int] = None
lineapersonaaa: Optional[int] = None
calamebasemexmp: Optional[int] = None
calamebasemexva: Optional[int] = None
calamebasemexempaque: Optional[int] = None
usarvaenfacturaamericana: Optional[int] = None
activarfechacortesaldos: Optional[int] = None
fechacortesaldos: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
respaldardescargaendescargam: Optional[int] = None
ocultarfechahora: Optional[int] = None
calcularvaloresamericanosenbaseapartida: Optional[int] = None
validarestatusped: Optional[int] = None
descapais: Optional[int] = None
class SSisImpoSettings(BaseModel):
"""Legacy Table: SSisImpo (General Import)"""
consecutivo: Optional[int] = None
prefijoimpo: Optional[str] = None
prefijocr: Optional[str] = None
consecutivoimpo: Optional[str] = None
consecutivocr: Optional[int] = None
porparteclasemex: Optional[str] = None
porparteclaseame: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
numpartemex: Optional[int] = None
numparteame: Optional[int] = None
firmafmex: Optional[str] = None
firmafame: Optional[str] = None
codigobarras: Optional[int] = None
fraccionimp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
paisorigenmex: Optional[int] = None
umequivalentemex: Optional[int] = None
umauxiliarmex: Optional[int] = None
perpagrenglon: Optional[int] = None
fraccioname: Optional[int] = None
paisorigename: Optional[int] = None
umequivalenteame: Optional[int] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
clavemoneda: Optional[str] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
valorlimitepar: Optional[Decimal] = None
umalternamex: Optional[int] = None
restringeimpopt: Optional[int] = None
controlremesa: Optional[str] = None
remesainicio: Optional[int] = None
remesafinal: Optional[int] = None
leyendafacame: Optional[str] = None
calcigibasecapt: Optional[int] = None
umequivalentemex2: Optional[int] = None
umequivalenteame2: Optional[int] = None
partecomplementariamex: Optional[int] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
metvalor: Optional[str] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
packingpedclave: Optional[int] = None
porordenporocpack: Optional[str] = None
actualizarpartepartida: Optional[int] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
clavebultos: Optional[str] = None
partecomplemexpartida: Optional[int] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
valorlimiteparmin: Optional[Decimal] = None
packingordencompra: Optional[int] = None
firmaelectronica: Optional[str] = None
solicitarcontrasenaadministrador: Optional[int] = None
incluirlineadelpo: Optional[int] = None
lineapersonaaa: Optional[int] = None
agregarvaenprodterminados: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
ocultarfechahora: Optional[int] = None
class QSisCMexSettings(BaseModel):
"""Legacy Table: QSisCMex (Fixed Asset Mexican Purchase)"""
consecutivo: Optional[int] = None
prefijocm: Optional[str] = None
consecutivocm: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
flete: Optional[Decimal] = None
paisorigenmex: Optional[int] = None
firmafmex: Optional[str] = None
fraccionimp: Optional[int] = None
tipofraccmex: Optional[int] = None
tasafraccmex: Optional[int] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
clavemoneda: Optional[str] = None
transportista: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
packingordencompra: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
class QSisDefSettings(BaseModel):
"""Legacy Table: QSisDef (Fixed Asset Definitive Import)"""
consecutivo: Optional[int] = None
prefijoimpo: Optional[str] = None
consecutivoimpo: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
firmafmex: Optional[str] = None
firmafame: Optional[str] = None
generarassettag: Optional[int] = None
codigobarras: Optional[int] = None
fraccionimp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
fraccioname: Optional[int] = None
paisorigenmex: Optional[int] = None
paisorigename: Optional[int] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
tipomn: Optional[str] = None
tipome: Optional[str] = None
clavemoneda: Optional[str] = None
controlremesa: Optional[str] = None
remesainicio: Optional[int] = None
remesafinal: Optional[int] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
metvalor: Optional[str] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
numpartemex: Optional[int] = None
numparteame: Optional[int] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
actualizarpartepartida: Optional[int] = None
packingordencompra: Optional[int] = None
solicitarcontrasenaadministrador: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
class QSisGenSettings(BaseModel):
"""Legacy Table: QSisGen (Fixed Asset General)"""
consecutivo: Optional[int] = None
dta: Optional[int] = None
dtaexpo: Optional[int] = None
subempresa: Optional[str] = None
patharch: Optional[str] = None
patharchtransmision: Optional[str] = None
pathrespuesta: Optional[str] = None
patharchped: Optional[str] = None
patharchpedconsm: Optional[str] = None
pathgenimpotemp: Optional[str] = None
pathgenexpo: Optional[str] = None
aplicapermat: Optional[int] = None
actseguridad: Optional[int] = None
controldes: Optional[int] = None
diadesactual: Optional[int] = None
fechades: Optional[int] = None
ubiplanta: Optional[str] = None
datoshistoricos: Optional[int] = None
tipovenro: Optional[str] = None
cantvenro: Optional[int] = None
omitirimposubpcodbarras: Optional[str] = None
muestraarchcodbarras: Optional[int] = None
calvalbasetcped: Optional[int] = None
calvalbasetcpedexpo: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
factoriva: Optional[Decimal] = None
filtrocantidad: Optional[Decimal] = None
firmapacking: Optional[int] = None
advertenciatm: Optional[int] = None
calcdepreciacion: Optional[str] = None
valmanifusado: Optional[int] = None
validadecencant: Optional[int] = None
mostraradvertenciaro: Optional[int] = None
usartranspamedocame: Optional[int] = None
escondaamexpacking: Optional[int] = None
cantvscantseries: Optional[int] = None
covefechaemision: Optional[int] = None
interfaceaaconsolidada: Optional[int] = None
interfaceaatcfpff: Optional[str] = None
incluirobscoveobsimpo: Optional[int] = None
agregarincreimpo: Optional[int] = None
repdescargolineal: Optional[int] = None
identificadornodoseriecove: Optional[int] = None
actpdfreportes: Optional[int] = None
patharchpdfimpo: Optional[str] = None
patharchpdfexpo: Optional[str] = None
mensajesvurfc: Optional[int] = None
mostrarpackinglistingles: Optional[int] = None
enviarsubpartidascove: Optional[int] = None
restringepaisimpo: Optional[int] = None
restringpaisexpo: Optional[int] = None
utilizarfechapagopeddeundiaanterior: Optional[int] = None
utilizartitulosalternativosimpresionfactura: Optional[int] = None
usartcdelafechapagopedimpoendescarga: Optional[int] = None
usarvude128o256: Optional[int] = None
agregarnumeroembarque: Optional[int] = None
utilizarumdeexistenciaentransmisionvu: Optional[int] = None
utilizarcodigodebrokerdeclienteenmainx30: Optional[int] = None
hojacalculosepararincrementablesanexo3: Optional[int] = None
hojacalculodesglosefacturaanexo3: Optional[int] = None
utilizarnombregenericomainx30: Optional[int] = None
utilizartcrespectoatipoped: Optional[int] = None
utilizarsolopartesnaftaenco: Optional[int] = None
cambiarpesosporcostounitario: Optional[int] = None
bloqueodeediciondefacturas: Optional[int] = None
calcularcostounitarioenbaseavalortotalscaf: Optional[int] = None
impresionfacturaalterna: Optional[int] = None
transmitirfacalterna: Optional[int] = None
class QSisExpoRepSettings(BaseModel):
"""Legacy Table: QSisExpoRep (Fixed Asset Export Repeat?)"""
tipofactura: Optional[str] = None
prefijoexpo: Optional[str] = None
consecutivoexpo: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
vendidopor: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
codigobarras: Optional[int] = None
cantpncodbar: Optional[int] = None
dtacerocodbar: Optional[int] = None
cbarrasvalor: Optional[str] = None
firmafmex: Optional[str] = None
fraccionexp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
numpartemex: Optional[int] = None
paisorigenmex: Optional[int] = None
firmafame: Optional[str] = None
fraccioname: Optional[int] = None
paisorigename: Optional[int] = None
numparteame: Optional[int] = None
codigoscac: Optional[int] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
clavemoneda: Optional[str] = None
tipomn: Optional[str] = None
tipome: Optional[str] = None
mostrarvalact: Optional[int] = None
pepsporclase: Optional[int] = None
pepsporfraccion: Optional[int] = None
pepspordescripcion: Optional[int] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
ventqueueact: Optional[int] = None
valfacttc: Optional[str] = None
metvalor: Optional[str] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
enviadoporvendidopor: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
proveedorexportador: Optional[str] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
solicitarpswdactual: Optional[int] = None
solicitarpswddesactual: Optional[int] = None
packingordenventa: Optional[int] = None
imprimirpo: Optional[int] = None
fdainformacioname: Optional[int] = None
packingpedclave: Optional[int] = None
solicitarcontrasenaadministrador: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
class QSisImpoSettings(BaseModel):
"""Legacy Table: QSisImpo (Fixed Asset Import)"""
consecutivo: Optional[int] = None
prefijoimpo: Optional[str] = None
consecutivoimpo: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
firmafmex: Optional[str] = None
firmafame: Optional[str] = None
codigobarras: Optional[int] = None
fraccionimp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
fraccioname: Optional[int] = None
paisorigenmex: Optional[int] = None
paisorigename: Optional[int] = None
impordencomp: Optional[int] = None
generarassettag: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
tipomn: Optional[str] = None
tipome: Optional[str] = None
clavemoneda: Optional[str] = None
controlremesa: Optional[str] = None
remesainicio: Optional[int] = None
remesafinal: Optional[int] = None
perpagrenglon: Optional[int] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
metvalor: Optional[str] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
numpartemex: Optional[int] = None
numparteame: Optional[int] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
actualizarpartepartida: Optional[int] = None
packingordencompra: Optional[int] = None
solicitarcontrasenaadministrador: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
class QSisImpoRepSettings(BaseModel):
"""Legacy Table: QSisImpoRep (Fixed Asset Import Repeat?)"""
tipofactura: Optional[str] = None
consecutivo: Optional[int] = None
prefijoimpo: Optional[str] = None
consecutivoimpo: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
firmafmex: Optional[str] = None
firmafame: Optional[str] = None
codigobarras: Optional[int] = None
fraccionimp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
fraccioname: Optional[int] = None
paisorigenmex: Optional[int] = None
paisorigename: Optional[int] = None
impordencomp: Optional[int] = None
generarassettag: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
tipomn: Optional[str] = None
tipome: Optional[str] = None
clavemoneda: Optional[str] = None
controlremesa: Optional[str] = None
remesainicio: Optional[int] = None
remesafinal: Optional[int] = None
perpagrenglon: Optional[int] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
ventqueueact: Optional[int] = None
metvalor: Optional[str] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
numpartemex: Optional[int] = None
numparteame: Optional[int] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
packingordencompra: Optional[int] = None
solicitarcontrasenaadministrador: Optional[int] = None
class QSisExpoSettings(BaseModel):
"""Legacy Table: QSisExpo (Fixed Asset Export)"""
tipofactura: Optional[str] = None
escambioregimen: Optional[str] = None
prefijoexpo: Optional[str] = None
consecutivoexpo: Optional[str] = None
pedimento: Optional[str] = None
tipo: Optional[str] = None
proveedor: Optional[str] = None
vendidoconsignado: Optional[str] = None
vendidoa: Optional[str] = None
enviadotransferido: Optional[str] = None
enviadoa: Optional[str] = None
vendidopor: Optional[str] = None
aaduanal: Optional[str] = None
incoterm: Optional[str] = None
flete: Optional[Decimal] = None
identificador: Optional[str] = None
codigobarras: Optional[int] = None
cantpncodbar: Optional[int] = None
dtacerocodbar: Optional[int] = None
cbarrasvalor: Optional[str] = None
firmafmex: Optional[str] = None
fraccionexp: Optional[int] = None
tasafraccmex: Optional[int] = None
tipofraccmex: Optional[int] = None
numpartemex: Optional[int] = None
paisorigenmex: Optional[int] = None
costounitmex: Optional[int] = None
ocultarempaquemex: Optional[int] = None
firmafame: Optional[str] = None
fraccioname: Optional[int] = None
paisorigename: Optional[int] = None
fdainformacioname: Optional[int] = None
numparteame: Optional[int] = None
codigoscac: Optional[int] = None
impordencomp: Optional[int] = None
decimalespeso: Optional[int] = None
decimalescant: Optional[int] = None
decimalesvalor: Optional[int] = None
decimalescosto: Optional[int] = None
tipomoneda: Optional[str] = None
clavemoneda: Optional[str] = None
tipomn: Optional[str] = None
tipome: Optional[str] = None
mostrarvalact: Optional[int] = None
pepsporclase: Optional[int] = None
pepsporfraccion: Optional[int] = None
pepspordescripcion: Optional[int] = None
pepsporfraccalterna: Optional[int] = None
cantlimite: Optional[Decimal] = None
pesolimite: Optional[Decimal] = None
valorlimite: Optional[Decimal] = None
ventqueueact: Optional[int] = None
valfacttc: Optional[str] = None
metvalor: Optional[str] = None
pagoimpuesto: Optional[str] = None
formapago: Optional[str] = None
colcantporpeso: Optional[int] = None
transportista: Optional[str] = None
aaduanalame: Optional[str] = None
enviadoporvendidopor: Optional[str] = None
conductor: Optional[str] = None
transporte: Optional[str] = None
numtransporte: Optional[str] = None
observacione: Optional[str] = None
observacioni: Optional[str] = None
proveedorexportador: Optional[str] = None
cantlimitemin: Optional[Decimal] = None
pesolimitemin: Optional[Decimal] = None
valorlimitemin: Optional[Decimal] = None
solicitarpswdactual: Optional[int] = None
solicitarpswddesactual: Optional[int] = None
packingordenventa: Optional[int] = None
imprimirpo: Optional[int] = None
packingpedclave: Optional[int] = None
generafacturaimd: Optional[str] = None
generafacturaimdenbasea: Optional[str] = None
solicitarcontrasenaadministrador: Optional[int] = None
imprimirlote: Optional[int] = None
imprimirnumentrada: Optional[int] = None
class SettingsPayload(BaseModel):
"""
Master container for all possible overrides.
Using Optional fields so only the delta is required in JSONB.
"""
# SCAII (Invoices / Inventory)
ssisgen: Optional[SSisGenSettings] = None
ssisgen2: Optional[SSisGen2Settings] = None
ssisgen3: Optional[SSisGen3Settings] = None
ssismex: Optional[SSisMexSettings] = None
ssisdef: Optional[SSisDefSettings] = None
ssisexpo: Optional[SSisExpoSettings] = None
ssisimpo: Optional[SSisImpoSettings] = None
# SCAF (Fixed Assets)
qsiscmex: Optional[QSisCMexSettings] = None
qsisdef: Optional[QSisDefSettings] = None
qsisgen: Optional[QSisGenSettings] = None
qsisexporep: Optional[QSisExpoRepSettings] = None
qsisimpo: Optional[QSisImpoSettings] = None
qsisimporep: Optional[QSisImpoRepSettings] = None
qsisexpo: Optional[QSisExpoSettings] = None
class AppSettingRequest(BaseModel):
tenant_id: Optional[int] = None
company_id: Optional[int] = None
settings: SettingsPayload
class AppSettingResponse(BaseModel):
id: int
tenant_id: Optional[int]
company_id: Optional[int]
settings: Dict[str, Any]
class Config:
from_attributes = True

View File

@@ -0,0 +1,137 @@
from typing import Optional, List, Dict, Any
from sqlalchemy import or_, and_, select, case, nulls_first
from sqlalchemy.orm import Session
from .models import AppSetting
import logging
logger = logging.getLogger(__name__)
from decimal import Decimal
def convert_decimals(obj: Any) -> Any:
"""
Recursively converts Decimal objects to floats for JSON serialization.
"""
if isinstance(obj, list):
return [convert_decimals(i) for i in obj]
elif isinstance(obj, dict):
return {k: convert_decimals(v) for k, v in obj.items()}
elif isinstance(obj, Decimal):
return float(obj)
return obj
def deep_merge(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively merges dict2 into dict1.
"""
for key, value in dict2.items():
if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict):
deep_merge(dict1[key], value)
else:
dict1[key] = value
return dict1
class AppSettingsService:
"""
Service to manage hierarchical configuration overrides.
Hierarchy: System (Global) -> Tenant -> Company.
"""
@staticmethod
def get_resolved_settings(db: Session, tenant_id: int, company_id: int) -> Dict[str, Any]:
"""
Retrieves settings from all levels (Global -> Tenant -> Company) and merges them.
"""
stmt = (
select(AppSetting)
.where(
or_(
and_(AppSetting.tenant_id.is_(None), AppSetting.company_id.is_(None)),
and_(AppSetting.tenant_id == tenant_id, AppSetting.company_id.is_(None)),
and_(AppSetting.tenant_id == tenant_id, AppSetting.company_id == company_id),
)
)
.order_by(
# Ensure the order is: Global (1) -> Tenant (2) -> Company (3)
case(
(and_(AppSetting.tenant_id.is_(None), AppSetting.company_id.is_(None)), 1),
(and_(AppSetting.tenant_id.is_not(None), AppSetting.company_id.is_(None)), 2),
(and_(AppSetting.tenant_id.is_not(None), AppSetting.company_id.is_not(None)), 3),
else_=4
).asc()
)
)
results = db.execute(stmt).scalars().all()
logger.info(f"RESOLVE: Found {len(results)} rows for Hierarchy")
resolved_settings = {}
for row in results:
level_name = "GLOBAL" if not row.tenant_id else ("TENANT" if not row.company_id else "COMPANY")
# Use safe data logging to avoid crashes
settings_data = row.settings if row.settings else {}
keys = list(settings_data.keys()) if isinstance(settings_data, dict) else "not-a-dict"
logger.info(f"RESOLVE: Merging {level_name} layer with keys: {keys}")
if isinstance(settings_data, dict):
deep_merge(resolved_settings, settings_data)
return resolved_settings
@staticmethod
def upsert_settings(db: Session, tenant_id: Optional[int], company_id: Optional[int], settings: Dict[str, Any]) -> AppSetting:
"""
Inserts or updates settings for a specific level.
"""
level_label = f"level(tenant={tenant_id}, company={company_id})"
logger.info(f"UPSERT Settings START: {level_label}, keys_to_update={list(settings.keys())}")
# Ensure all Decimals are converted to floats before deep merge and save
settings = convert_decimals(settings)
stmt = select(AppSetting).where(
and_(
AppSetting.tenant_id == tenant_id if tenant_id is not None else AppSetting.tenant_id.is_(None),
AppSetting.company_id == company_id if company_id is not None else AppSetting.company_id.is_(None)
)
)
existing = db.execute(stmt).scalar_one_or_none()
if existing:
# Deep merge at the root level (merging categories like ssisgen, ssismex, etc.)
logger.info(f"UPSERT: Updating existing row ID={existing.id}")
# Create a shallow copy of the top-level dict to ensure SQLAlchemy sees a new reference
new_settings = dict(existing.settings) if existing.settings else {}
# Detailed logging of what's changing
for cat, data in settings.items():
old_keys = list(new_settings.get(cat, {}).keys())
new_keys = list(data.keys()) if isinstance(data, dict) else []
logger.info(f"UPSERT: Merging category [{cat}]. Old keys: {old_keys}, New keys to merge/overwrite: {new_keys}")
deep_merge(new_settings, settings)
# Second pass of conversion (merged results might still have Decimals if original row had them)
existing.settings = convert_decimals(new_settings)
from sqlalchemy.orm.attributes import flag_modified
flag_modified(existing, "settings")
else:
logger.info(f"UPSERT: Creating NEW row for {level_label}")
existing = AppSetting(
tenant_id=tenant_id,
company_id=company_id,
settings=settings
)
db.add(existing)
try:
db.commit()
db.refresh(existing)
logger.info(f"UPSERT SUCCESS: Row ID={existing.id}, Final Settings Hash Keys={list(existing.settings.keys())}")
except Exception as e:
db.rollback()
logger.error(f"UPSERT FAILED: {str(e)}")
raise e
return existing

View File

@@ -47,6 +47,7 @@ from .reports.exportacion.transmission.MAINX30.routes import router as transmiss
from .reports.importacion.transmission.temporal.MAINX30.routes import router as transmission_temporal_router
from .reports.importacion.transmission.definitive.MAINX30.routes import router as transmission_definitive_router
from .reports.importacion.winsaai.router import router as winsaai_router
from .app_settings.routes import router as app_settings_router
from .manifests.manifest.routes import router as manifests_router
from .manifests.driver.routes import router as manifest_drivers_router
@@ -176,6 +177,8 @@ router.include_router(
tags=["a76 / reports"]
)
router.include_router(app_settings_router)
# Registrar router de bitácora
from .audit_log.router import router as audit_log_router
router.include_router(audit_log_router, prefix="/a76/audit-log", tags=["Audit Log"])

View File

@@ -0,0 +1,34 @@
import { BACKEND_URL } from '$lib/config/backend';
export interface AppSettingsRequest {
tenant_id?: number | null;
company_id?: number | null;
settings: Record<string, any>;
}
export const appSettingsApi = {
/**
* Resolves settings merging Global -> Tenant -> Company hierarchy
*/
async getResolved(tenantId: number, companyId: number): Promise<Record<string, any>> {
const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/resolved?tenant_id=${tenantId}&company_id=${companyId}`);
if (!response.ok) throw new Error('Error al obtener configuraciones');
return response.json();
},
/**
* Upserts an override at a specific level
*/
async upsert(payload: AppSettingsRequest): Promise<any> {
const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/upsert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Error al guardar configuración');
}
return response.json();
}
};

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { SETTINGS_METADATA } from './settings-metadata';
import SettingFormField from './SettingFormField.svelte';
import { Input } from '$lib/components/ui/input';
import { Search, FilterX } from 'lucide-svelte';
import { fade } from 'svelte/transition';
export let category: string;
export let currentData: any = {};
export let onUpdate: (data: any) => void;
let searchQuery = '';
// Local state to manage edits before final save
let formData: any = { ...currentData };
// Update local state when category changes
$: {
formData = { ...currentData };
}
function handleFieldChange(key: string, value: any) {
formData[key] = value;
onUpdate(formData); // Propagate changes upward
}
// Filter fields based on metadata and search query
$: availableFields = SETTINGS_METADATA[category] || Object.keys(currentData);
$: filteredFields = availableFields.filter(f =>
f.toLowerCase().includes(searchQuery.toLowerCase())
);
// Derived grouped fields for UI organization
$: hasResults = filteredFields.length > 0;
</script>
<div class="space-y-6 flex flex-col h-full overflow-hidden">
<!-- Search/Filter Bar -->
<div class="relative group mx-2">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-primary transition-colors" />
<Input
placeholder="Buscar parámetro en {category}..."
bind:value={searchQuery}
class="pl-10 bg-black/40 border-white/5 h-10 focus-visible:ring-primary/20"
/>
</div>
<!-- Fields Grid -->
<div class="flex-1 overflow-y-auto px-2 custom-scrollbar pr-4">
{#if hasResults}
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 pb-8">
{#each filteredFields as field (field)}
<div in:fade={{ duration: 150 }}>
<SettingFormField
key={field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
</div>
{/each}
</div>
{:else}
<div class="flex flex-col items-center justify-center h-48 text-white/20 border-2 border-dashed border-white/5 rounded-2xl mx-2">
<FilterX class="w-12 h-12 mb-4 opacity-50" />
<p class="text-xs uppercase tracking-widest">No se encontraron parámetros</p>
</div>
{/if}
</div>
</div>
<style>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
</style>

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import {
Settings, Package, Truck, Construction,
TrendingUp, FileText, ClipboardList, Database,
ChevronRight, Layers
} from 'lucide-svelte';
import { fade, slide } from 'svelte/transition';
export let activeCategory: string;
export let onSelect: (cat: string) => void;
const groups = [
{
title: 'SCAII (Inventarios)',
icon: Package,
items: [
{ id: 'ssisgen', label: 'Parámetros Generales', icon: Settings },
{ id: 'ssisgen2', label: 'Valor Agregado', icon: TrendingUp },
{ id: 'ssisgen3', label: 'Otros Parámetros', icon: ClipboardList },
{ id: 'ssismex', label: 'Nacional (MEX)', icon: FileText },
{ id: 'ssisdef', label: 'Imp. Definitiva', icon: Truck },
{ id: 'ssisimpo', label: 'Imp. Temporal', icon: Database },
{ id: 'ssisexpo', label: 'Exportación', icon: Construction },
]
},
{
title: 'SCAF (Activos Fijos)',
icon: Layers,
items: [
{ id: 'qsisgen', label: 'General Activos', icon: Settings },
{ id: 'qsiscmex', label: 'Mantenimiento EX', icon: FileText },
{ id: 'qsisdef', label: 'Imp. Definitiva', icon: Truck },
{ id: 'qsisimpo', label: 'Imp. Temporal', icon: Database },
{ id: 'qsisexpo', label: 'Exportación', icon: Construction },
{ id: 'qsisimporep', label: 'Rep. Importación', icon: ClipboardList },
{ id: 'qsisexporep', label: 'Rep. Exportación', icon: ClipboardList },
]
}
];
</script>
<div class="space-y-6">
{#each groups as group}
<div class="space-y-2">
<h3 class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground px-4 flex items-center gap-2">
<svelte:component this={group.icon} class="w-3 h-3" />
{group.title}
</h3>
<div class="space-y-1">
{#each group.items as item}
<button
class="w-full flex items-center justify-between gap-3 px-4 py-2.5 rounded-lg transition-all group
{activeCategory === item.id
? 'bg-primary/10 text-primary border border-primary/20 shadow-lg shadow-primary/5'
: 'text-white/60 hover:bg-white/5 border border-transparent hover:border-white/5'}"
onclick={() => onSelect(item.id)}
>
<div class="flex items-center gap-3">
<svelte:component this={item.icon} class="w-4 h-4 {activeCategory === item.id ? 'text-primary' : 'text-white/40 group-hover:text-white/60'}" />
<span class="text-sm font-medium">{item.label}</span>
</div>
{#if activeCategory === item.id}
<div in:fade>
<ChevronRight class="w-4 h-4 text-primary" />
</div>
{/if}
</button>
{/each}
</div>
</div>
{/each}
</div>

View File

@@ -0,0 +1,168 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Switch } from '$lib/components/ui/switch';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { FolderOpen } from 'lucide-svelte';
interface Props {
key: string;
label?: string;
value: any;
onChange: (val: any) => void;
options?: { value: any, label: string }[];
type?: 'switch' | 'input' | 'select' | 'path' | 'radio' | 'password' | 'number';
hint?: string;
disabled?: boolean;
}
let { key, label, value, onChange, options, type, hint, disabled }: Props = $props();
// Heuristic to detect toggleable (boolean-like) legacy fields (0/1)
const isToggleable = (k: string, v: any) => {
const toggles = [
'act', 'valid', 'mostr', 'usar', 'utiliz', 'restring', 'bloqueo',
'omiti', 'oculta', 'resaltar', 'cal', 'mues', 'mens', 'cont', 'firm',
'esc', 'adv', 'val', 'tom', 'nom', 'rev', 'imp', 'temp', 'costo',
'des', 'asig', 'calc', 'param', 'fracc', 'valor', 'cove', 'loc', 'inter', 'inclu', 'actvalor', 'trans'
];
// Be more lenient: if it has the prefix and is 0, 1, or null, it's likely a toggle
return (v === 0 || v === 1 || v === null || v === undefined) &&
toggles.some(t => k.toLowerCase().startsWith(t));
}
const isPath = $derived(
key.toLowerCase().includes('path') ||
key.toLowerCase().includes('arch') ||
key.toLowerCase().includes('ruta')
);
// Heuristic or explicit type
let mode = $derived(type || (options ? 'select' : (isToggleable(key, value) ? 'switch' : (isPath ? 'path' : 'input'))));
function handleBrowse() {
try {
if (typeof window !== 'undefined' && 'showDirectoryPicker' in window) {
// @ts-ignore
window.showDirectoryPicker().then((handle: any) => {
onChange(handle.name);
}).catch((err: any) => console.log("Browse cancelled:", err));
}
} catch (err) {
console.log("Browse failed:", err);
}
}
</script>
<div class="flex flex-col gap-2.5 p-4 rounded-xl bg-white/5 border border-white/5 hover:border-primary/30 transition-all group">
<div class="flex flex-col gap-2 {mode === 'switch' ? 'flex-row items-center justify-between md:flex-row' : ''}">
<Label for={key} class="text-sm font-medium {mode === 'switch' ? 'text-white/80 cursor-pointer lg:max-w-[70%]' : 'text-white/40 ml-1'} {disabled ? 'opacity-30 cursor-not-allowed' : ''}">
{label || key}
</Label>
{#if mode === 'select' && options}
<Select.Root
type="single"
value={String(value)}
onValueChange={onChange}
disabled={disabled}
>
<Select.Trigger class="w-full bg-[#0f0f0f] border-white/10 text-white rounded-xl focus:ring-primary/40 h-11 px-4">
<Select.Value placeholder="Seleccionar..." />
</Select.Trigger>
<Select.Content class="bg-[#0f0f0f] border-white/10 text-white min-w-[200px]">
{#each options as opt}
<Select.Item value={String(opt.value)} label={opt.label} class="hover:bg-primary/20 focus:bg-primary/20 text-white">
{opt.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{:else if mode === 'radio'}
<RadioGroup.Root
value={String(value)}
onValueChange={onChange}
disabled={disabled}
class="flex flex-col gap-3"
>
{#each options || [] as opt}
<div class="flex items-center space-x-3">
<RadioGroup.Item value={String(opt.value)} id={`${key}-${opt.value}`} />
<Label for={`${key}-${opt.value}`} class="text-sm font-medium text-white/70 cursor-pointer {disabled ? 'cursor-not-allowed' : ''}">
{opt.label}
</Label>
</div>
{/each}
</RadioGroup.Root>
{:else if mode === 'switch'}
<Switch
id={key}
checked={value === 1 || value === '1' || value === true || value === 'true'}
onCheckedChange={(val) => onChange(val ? 1 : 0)}
disabled={disabled}
/>
{:else}
<div class="relative group space-y-1.5">
{#if mode === 'path'}
<div class="flex gap-2">
<Input
id={key}
value={value || ""}
oninput={(e: any) => onChange(e.target.value)}
placeholder="Ruta del directorio..."
disabled={disabled}
class="flex-1 bg-[#0f0f0f] border-white/5 text-white rounded-xl focus:ring-primary/40 h-11 px-4 pr-10 hover:border-white/20 transition-all font-mono text-xs"
/>
<Button
variant="outline"
size="icon"
disabled={disabled}
onclick={handleBrowse}
class="h-11 w-11 shrink-0 bg-white/[0.03] border-white/10 hover:bg-white/[0.08] hover:border-primary/50 text-white transition-all shadow-xl"
>
<FolderOpen class="w-4 h-4 text-primary" />
</Button>
</div>
{:else if mode === 'password'}
<Input
id={key}
type="password"
value={value || ""}
oninput={(e: any) => onChange(e.target.value)}
placeholder="••••••••"
disabled={disabled}
class="bg-[#0f0f0f] border-white/5 text-white rounded-xl focus:ring-primary/40 h-11 px-4 hover:border-white/20 transition-all w-full"
/>
{:else if mode === 'number'}
<Input
id={key}
type="number"
value={value || ""}
oninput={(e: any) => onChange(Number(e.target.value))}
disabled={disabled}
class="bg-[#0f0f0f] border-white/5 text-white rounded-xl focus:ring-primary/40 h-11 px-4 hover:border-white/20 transition-all w-full text-right"
/>
{:else}
<Input
id={key}
value={value || ""}
oninput={(e: any) => onChange(e.target.value)}
disabled={disabled}
class="bg-[#0f0f0f] border-white/5 text-white rounded-xl focus:ring-primary/40 h-11 px-4 hover:border-white/20 transition-all"
/>
{/if}
{#if hint}
<p class="text-[10px] text-white/30 ml-1 font-medium italic">
{hint}
</p>
{/if}
</div>
{/if}
</div>
</div>
<style>
/* Minimal styling for radio group items if needed */
</style>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { Globe, Database, Layers } from 'lucide-svelte';
export let level: 'global' | 'tenant' | 'company';
export let size: 'sm' | 'md' = 'md';
const config = {
global: {
label: 'Global',
icon: Globe,
color: 'text-primary bg-primary/10 border-primary/20'
},
tenant: {
label: 'Tenant',
icon: Database,
color: 'text-blue-400 bg-blue-400/10 border-blue-400/20'
},
company: {
label: 'Company',
icon: Layers,
color: 'text-purple-400 bg-purple-400/10 border-purple-400/20'
}
};
$: current = config[level];
</script>
<div class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border {current.color} {size === 'sm' ? 'text-[10px]' : 'text-xs'} font-medium">
<svelte:component this={current.icon} class={size === 'sm' ? 'w-3 h-3' : 'w-3.5 h-3.5'} />
{current.label.toUpperCase()}
</div>

View File

@@ -0,0 +1,626 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import * as Tabs from '$lib/components/ui/tabs';
import SettingFormField from './SettingFormField.svelte';
interface Props {
currentData: any;
onUpdate: (data: any) => void;
}
let { currentData = {}, onUpdate }: Props = $props();
let formData: any = $state({ ...currentData });
// Initialize local/loc fields if they don't exist
$effect(() => {
const defaults = {
DownloadFTP: formData.DownloadFTP || 'No',
DescargarFTPoLocal: formData.DescargarFTPoLocal || 'FTP',
MinsDownLFTP: formData.MinsDownLFTP || 0,
};
// Object.assign(formData, { ...defaults, ...currentData });
formData = { ...defaults, ...currentData };
});
function handleFieldChange(key: string, value: any) {
formData[key] = value;
onUpdate(formData);
}
// Translation Mapping for SSisGen
const FIELD_LABELS: Record<string, string> = {
consecutivo: "Número Consecutivo",
dta: "DTA (Derecho Trámite Aduanero)",
dtaexpo: "DTA Exportación",
subempresa: "Sub-Empresa / División",
patharch: "Generación de los archivos PDF por",
patharchtransmision: "Generación de archivos electrónicos por transmitir en",
pathtransexpo: "Ruta: Transmisión Exportación",
pathrespuesta: "Ruta: Respuestas SAAI",
patharchped: "Generación de archivos electrónicos para pedimentos en",
patharchpedconsm: "Ruta de los archivos TXT de los archivos previos para pedimentos consolidados",
pathgenimpotemp: "Ruta de los archivos CSV para importaciones",
pathgenexpo: "Ruta de los archivos CSV para exportación",
actseguridad: "Activar la seguridad del sistema",
controldes: "Asignar fecha límite para desactualizar facturas",
diadesactual: "Días para desactualizar facturas (Impo/Expo)",
diavencimiento: "Días de anticipación (vencimiento)",
mensajevenc: "Activar la advertencia de saldos vencidos",
fechades: "Fecha de Desactualización Automática",
factoriva: "Factor IVA (Aplica en compras)",
validasifra: "Validar Fracciones con SIFRA",
decimalespeso: "Peso neto y bruto",
decimalescant: "Cantidades",
decimalesvalor: "Valores y costos",
calvalbasetcped: "Cálculo en base al TC fecha pago (Importación)",
calvalbasetcpedexpo: "Cálculo en base al TC fecha pago (Exportación)",
filtrocantidad: "Omitir Cantidades con balance menor a",
muestraarchcodbarras: "Muestra archivo TXT al generar factura/remesa",
datoshist: "Permitir Datos Históricos",
tipovenro: "Tipo de validación para regla octava",
cantvenro: "Cantidad para validación (Porcentaje/Días)",
costoplanta: "Costo Directo en Planta",
firmapacking: "Esconder la firma en el packing list",
escondaamexpacking: "Esconde el agente aduanal mexicano en el packing list",
advertenciatm: "Advertencia del uso del tipo de moneda en facturas",
costoimpofijo: "Valida como inconsistencia si el costo de la parte es diferente al costo de la partida al actualizar la factura de importación",
tomarsaldosvenc: "Activar si desea tomar saldos temporales o vencidos para descargar",
valmanifusado: "Activar control de asignación de manifiestos en factura",
valparteexiste: "No mostrar ventanas de auto selección de partes y marcar error en caso de que no exista",
temporalfechapago: "Tomar temporalidad en base a la fecha de pago del pedimento",
muestra_copias_codbarras: "Imprimir código de barras en las demas copias, aparte de la copia del transportista",
mostrarpackinglistingles: "Mostrar packing list en inglés",
activar_revision_fracciones: "Activar revisión de fracciones",
transmitirfacalterna: "Transmitir Factura Alterna en VU",
deshabilitardescparte: "Deshabilitar la Descripción en Español del Número de Parte",
deshabilitardescparteing: "Deshabilitar la Descripción en Inglés del Número de Parte",
asignafracameparte: "Asignar Automáticamente Fracción Americana de Expo US Dutible en Base a la información de la Clase y Parte",
validadecencant: "En las Partidas de Factura se Restringe la Captura de Decimales en el Campo de Unidad de Medida Pza",
mostraradvertenciaro: "Mostrar Advertencia si el Costo del Permiso de Regla Octava es Diferente al Costo de la Partida al Momento de Guardarla",
mostraradvertenciarovalor: "Mostrar Advertencia si el Valor del Permiso de Regla Octava es Diferente al Valor de la Partida al Momento de Guardarla",
usartranspamedocame: "Mostrar Transportista Americano en vez del Mexicano en Packing List y Bill Of Lading de Exportación",
calcdutypacking: "Agregar el Valor de US Packing como Complemento del Valor Dutible para Exportación.",
parammultiples: "Activar Uso de Parámetros Multiples en Catálogos de Facturas Impo/Expo.",
fraccnivelpais: "Activar la Asignación de Fracción a Nivel Parte-Pais.",
valordllstcfacturaexpo: "Mostrar Valor en dolares conforme al Tipo de Cambio de Exportación (Facturas, Códigos de Barra y COVE)",
covefechaemision: "Tomar la fecha de Emision para la generación del COVE.",
usarcoveenarchsaaim3: "Usar COVE en Lugar del Número de Factura en el Archivo de Transferencia SAAIM3.",
interfaceaaconsolidada: "Generar la Interface Consolidada por Clase para el Agente Aduanal.",
incluirobscoveobsimpo: "Incluir las Observaciones del COVE en las Observaciones en Importación.",
activarcatalogofraccionesamericanassifra: "Activar Catálogo Fracciones Americanas Sifra",
mostrarprogramaimmexprosec: "Mostrar Autorización Prosec en Reportes de Saldos y Descargos",
costounitarioporempaquefac: "Mostrar Costo Unitario Por Empaque en Impresión de Facturas de Exportación",
actvaloragre: "Activar Valor Agregado General",
valoragregadogen: "Valor Agregado General",
interfaceaatcfpff: "En Interfaces (WINSAAI, SAAIM3), Usar Tipo de Cambio de:",
agregarincreimpo: "Agregar Incrementables, Fecha de Emisión y Factor Tipo de Cambio en Facturas de Importación",
componentebom: "No Permitir Capturar Componentes Diferentes en BOMs (Materias Primas)",
limitesubensamble: "Límite Sub Ensamble",
partesypedimentosporcliente: "Agregar y Filtrar Catálogo de Partes y Pedimentos por Cliente",
mensajesvurfc: "Desactivar Mensajes de RFC en transmisión de VU",
omitirempaqueencodigobarras: "Omitir en el Código de Barras el Valor del Empaque",
actpdfreportes: "Generar PDFs al momento de Actualización",
patharchpdfimpo: "Generar Archivos PDF de Importación en:",
patharchpdfexpo: "Generar Archivos PDF de Exportación en:",
DownloadFTP: "¿Desea Activar Descarga Desde Sitio FTP o Localmente?",
MinsDownLFTP: "En Intervalos de (Minutos)",
DownloadFTPPath: "Descargar Archivos en",
DescargarFTPoLocal: "Descargar Archivos Desde",
ServerFTP: "Servidor",
UserFTP: "Usuario",
PasswordFTP: "Contraseña",
DirectorioFTP: "Directorio en FTP",
PathLocalParaDescDe: "Ruta Local",
AgregarRemplazarAutomatico: "Agregar/Remplazar Aut",
ActivarProcesoVEquipment: "Activar/Desactivar proceso para JD Edwards",
ActivarProcesoDesperdicioJDEdwards: "Activar proceso de desperdicio para JD Edwards"
};
// Grouped sections for the "General" tab
const GENERAL_SECTIONS = [
{
title: 'Parámetros del Sistema',
fields: ['factoriva', 'actseguridad']
},
{
title: 'Advertencias de Vencimiento',
fields: ['mensajevenc', 'diavencimiento']
},
{
title: 'Tipos de Cambio',
fields: ['calvalbasetcped', 'calvalbasetcpedexpo']
},
{
title: 'Desactualización de Facturas',
fields: ['controldes', 'diadesactual']
},
{
title: 'Número de decimales en reportes generales en',
fields: ['decimalespeso', 'decimalescant', 'decimalesvalor']
},
{
title: 'Filtrar por la cantidad',
fields: ['filtrocantidad']
},
{
title: 'Código de barras',
fields: ['muestraarchcodbarras']
}
];
// Grouped sections for "Archivos electrónicos" tab
const ARCHIVOS_SECTIONS = [
{
title: 'Gestión de Archivos PDF y Electrónicos',
fields: [
'patharch',
'patharchtransmision',
'patharchped',
'pathgenimpotemp',
'pathgenexpo',
'patharchpedconsm',
]
}
];
// Grouped sections for "Continuación" tab
const CONTINUACION_SECTIONS = [
{
title: 'Tipo de validación para regla octava',
fields: ['tipovenro', 'cantvenro']
},
{
title: 'Parámetros Adicionales y Visualización',
fields: [
'firmapacking',
'escondaamexpacking',
'advertenciatm',
'costoimpofijo',
'tomarsaldosvenc',
'valmanifusado',
'valparteexiste',
'temporalfechapago',
'muestra_copias_codbarras',
'mostrarpackinglistingles',
'activar_revision_fracciones'
]
}
];
// Grouped sections for "Cont 2" tab
const CONT2_SECTIONS = [
{
title: 'Configuraciones de Pantalla y Validación Avanzada',
fields: [
'deshabilitardescparte',
'deshabilitardescparteing',
'asignafracameparte',
'validadecencant',
'mostraradvertenciaro',
'mostraradvertenciarovalor',
'usartranspamedocame',
'calcdutypacking',
'parammultiples',
'fraccnivelpais',
'valordllstcfacturaexpo',
'covefechaemision',
'usarcoveenarchsaaim3',
'interfaceaaconsolidada',
'incluirobscoveobsimpo'
]
}
];
// Sections for "Cont 3" tab
const CONT3_SECTIONS = [
{
id: 'interface_tc',
title: 'Tipo de Cambio en Interfaces',
fields: ['interfaceaatcfpff']
},
{
id: 'config_general',
title: 'Opciones de Configuración General',
fields: [
'agregarincreimpo',
'componentebom',
'limitesubensamble',
'partesypedimentosporcliente',
'mensajesvurfc',
'omitirempaqueencodigobarras'
]
},
{
id: 'rutas_pdf',
title: 'Rutas y Archivos PDF',
fields: [
'actpdfreportes',
'patharchpdfimpo',
'patharchpdfexpo'
]
}
];
// Sections for "Cont 4" tab
const CONT4_SECTIONS = [
{
id: 'ftp_master',
title: 'Activación y Frecuencia',
fields: ['DownloadFTP', 'MinsDownLFTP']
},
{
id: 'ftp_destino',
title: 'Destino de Descarga',
fields: ['DownloadFTPPath']
},
{
id: 'ftp_origen',
title: 'Origen de Descarga',
fields: ['DescargarFTPoLocal']
},
{
id: 'ftp_config',
title: 'Configuración FTP',
fields: ['ServerFTP', 'UserFTP', 'PasswordFTP', 'DirectorioFTP']
},
{
id: 'local_config',
title: 'Configuración de Ruta Local',
fields: ['PathLocalParaDescDe']
},
{
id: 'additional_options',
title: 'Opciones Adicionales',
fields: ['AgregarRemplazarAutomatico', 'ActivarProcesoVEquipment', 'ActivarProcesoDesperdicioJDEdwards']
}
];
// Grouped sections for "Cont 7" tab
const CONT7_SECTIONS = [
{
title: 'Configuraciones de Fracciones y Transmisión',
fields: [
'activarcatalogofraccionesamericanassifra',
'mostrarprogramaimmexprosec',
'costounitarioporempaquefac',
'transmitirfacalterna'
]
}
];
const TABS = [
{ id: 'general', label: 'General' },
{ id: 'archivos', label: 'Archivos electrónicos' },
{ id: 'continuacion', label: 'Continuación' },
{ id: 'cont2', label: 'Cont 2' },
{ id: 'cont3', label: 'Cont 3' },
{ id: 'cont4', label: 'Cont 4' },
{ id: 'otros_parms', label: 'Otros Parámetros', fields: ["consecutivo", "dta", "dtaexpo", "subempresa", "fechades", "validasifra", "restringepaisimpo", "restringpaisexpo", "bloqueoaldesactivarnumerodeparte", "asignafracameparte", "validadecencant", "usartranspamedocame", "mostraradvertenciaro", "calcdutypacking", "parammultiples", "fraccnivelpais", "pathtransexpo", "pathrespuesta", "covefechaemision", "interfaceaaconsolidada", "interfaceaatcfpff", "incluirobscoveobsimpo", "agregarincreimpo", "componentebom", "limitesubensamble", "actpdfreportes", "patharchpdfimpo", "patharchpdfexpo", "noimprimircons", "mostraradvertenciarovalor", "partesypedimentosporcliente", "mensajesvurfc", "mostrarpackinglistingles", "omitirempaqueencodigobarras", "geninformeanexo31", "utilizarfechapagopeddeundiaanterior", "utilizarequivalenciasdeumpornumerodeparte", "utilizartitulosalternativosimpresionfactura"] },
{ id: 'cont5', label: 'cont 5', fields: ["utilizarnombregenericomainx30", "utilizartcrespectoatipoped", "tomardecimalescompletos", "incluirremesaeninterfazaawinsaai", "cambiarpesosporcostounitario", "utilizarsolopartesnaftaenco", "bloqueodeediciondefacturas"] },
{ id: 'cont6', label: 'cont 6', fields: ["reasignafraccionclase", "calcularcostounitarioenbaseavalortotal", "parametroauxiliar", "usarcontroldefechasdeversion", "desactivaciondemodulos"] },
{ id: 'cont7', label: 'cont 7' }
];
// Options for the rule validator select
const RULE_VALIDATOR_OPTIONS = [
{ value: 'Porcentaje', label: 'Porcentaje' },
{ value: 'Dias', label: 'Días' },
{ value: 'No aplica', label: 'No aplica' }
];
// Options for the interface TC radio buttons
const INTERFACE_TC_OPTIONS = [
{ value: 'P', label: 'Fecha de Pago' },
{ value: 'F', label: 'Fecha de Factura' }
];
// Options for radio groups in Cont 4
const YES_NO_OPTIONS = [
{ value: 'Si', label: 'Si' },
{ value: 'No', label: 'No' }
];
const FTP_LOCAL_OPTIONS = [
{ value: 'FTP', label: 'FTP' },
{ value: 'Ruta Local', label: 'Ruta Local' }
];
</script>
<div class="h-full flex flex-col bg-[#080808] rounded-2xl border border-white/5 overflow-hidden shadow-2xl">
<Tabs.Root value="general" class="flex flex-col h-full">
<Tabs.List class="flex flex-wrap gap-1 p-3 bg-black/60 border-b border-white/5 h-auto">
{#each TABS as tab}
<Tabs.Trigger
value={tab.id}
class="px-5 py-2.5 text-xs font-bold uppercase tracking-wider transition-all data-[state=active]:bg-primary data-[state=active]:text-white rounded-lg opacity-40 data-[state=active]:opacity-100 hover:opacity-70"
>
{tab.label}
</Tabs.Trigger>
{/each}
</Tabs.List>
<Tabs.Content value="general" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-12 min-h-[600px]">
{#each GENERAL_SECTIONS as section}
<div class="space-y-4">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
{/each}
</div>
</div>
{/each}
</div>
</Tabs.Content>
<Tabs.Content value="archivos" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-8 min-h-[600px]">
{#each ARCHIVOS_SECTIONS as section}
<div class="space-y-6 max-w-3xl mx-auto">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 gap-4">
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
{/each}
</div>
</div>
{/each}
</div>
</Tabs.Content>
<Tabs.Content value="continuacion" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-12 min-h-[600px]">
{#each CONTINUACION_SECTIONS as section}
<div class="space-y-4">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
options={field === 'tipovenro' ? RULE_VALIDATOR_OPTIONS : undefined}
/>
{/each}
</div>
</div>
{/each}
</div>
</Tabs.Content>
<Tabs.Content value="cont2" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-8 min-h-[600px]">
{#each CONT2_SECTIONS as section}
<div class="space-y-6 max-w-4xl mx-auto">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 gap-4">
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
{/each}
</div>
</div>
{/each}
</div>
</Tabs.Content>
<Tabs.Content value="cont3" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-12 min-h-[600px]">
{#each CONT3_SECTIONS as section}
<div class="space-y-6 max-w-4xl mx-auto">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 gap-4">
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
type={
field === 'interfaceaatcfpff' ? 'radio' :
(field === 'limitesubensamble' ? 'input' :
(['agregarincreimpo', 'componentebom', 'partesypedimentosporcliente'].includes(field) ? 'switch' : undefined))
}
options={field === 'interfaceaatcfpff' ? INTERFACE_TC_OPTIONS : undefined}
/>
{/each}
</div>
</div>
{/each}
</div>
</Tabs.Content>
<Tabs.Content value="cont4" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-8 min-h-[600px]">
<div class="max-w-4xl mx-auto space-y-12">
<!-- Master Activation (Blue Box Container) -->
<div class="space-y-6 p-8 rounded-3xl border-2 border-primary/20 bg-primary/[0.02] shadow-2xl transition-all {formData.DownloadFTP !== 'Si' ? 'grayscale opacity-60' : ''}">
{#each CONT4_SECTIONS.filter(s => s.id !== 'additional_options') as section}
<div class="space-y-6">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
disabled={section.id !== 'ftp_master' && formData.DownloadFTP !== 'Si'}
type={
field === 'DownloadFTP' || field === 'DescargarFTPoLocal' ? 'radio' :
field === 'MinsDownLFTP' ? 'number' :
field === 'PasswordFTP' ? 'password' :
undefined
}
options={
field === 'DownloadFTP' ? YES_NO_OPTIONS :
field === 'DescargarFTPoLocal' ? FTP_LOCAL_OPTIONS :
undefined
}
hint={
field === 'DirectorioFTP' ? 'Ejemplo: /Folder 1/SubFolder' :
field === 'PathLocalParaDescDe' ? 'Ejemplo: C:\\Aduanas\\SCAIISQL' :
undefined
}
{...(section.id === 'ftp_config' && formData.DescargarFTPoLocal === 'Ruta Local' ? { disabled: true } : {})}
{...(section.id === 'local_config' && formData.DescargarFTPoLocal === 'FTP' ? { disabled: true } : {})}
/>
{/each}
</div>
</div>
{/each}
</div>
<!-- Outside Options -->
<div class="space-y-6">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-white/40 whitespace-nowrap">
Opciones Adicionales
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-white/10 to-transparent"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each CONT4_SECTIONS.find(s => s.id === 'additional_options')?.fields || [] as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
{/each}
</div>
</div>
</div>
</div>
</Tabs.Content>
<Tabs.Content value="cont7" class="flex-1 overflow-hidden p-0 m-0 bg-transparent">
<div class="h-full w-full overflow-y-scroll custom-scrollbar p-6 space-y-8 min-h-[600px]">
{#each CONT7_SECTIONS as section}
<div class="space-y-6 max-w-4xl mx-auto">
<div class="flex items-center gap-4">
<h3 class="text-xs font-bold uppercase tracking-[0.2em] text-primary/80 whitespace-nowrap">
{section.title}
</h3>
<div class="h-[1px] w-full bg-gradient-to-r from-primary/20 to-transparent"></div>
</div>
<div class="grid grid-cols-1 gap-4">
<!-- Especial Row: Activar Valor Agregado Gral + Numeric Input -->
<div class="flex flex-col md:flex-row items-center gap-4 p-4 rounded-xl border border-white/5 bg-white/[0.02]">
<div class="flex-1">
<SettingFormField
key="actvaloragre"
label={FIELD_LABELS.actvaloragre}
value={formData.actvaloragre}
onChange={(val) => handleFieldChange('actvaloragre', val)}
/>
</div>
<div class="w-full md:w-32">
<SettingFormField
key="valoragregadogen"
label={FIELD_LABELS.valoragregadogen}
value={formData.valoragregadogen}
onChange={(val) => handleFieldChange('valoragregadogen', val)}
type="input"
/>
</div>
</div>
{#each section.fields as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
{/each}
</div>
</div>
{/each}
</div>
</Tabs.Content>
{#each TABS.filter(t => !['general', 'archivos', 'continuacion', 'cont2', 'cont3', 'cont4', 'cont7'].includes(t.id)) as tab}
<Tabs.Content value={tab.id} class="flex-1 overflow-hidden p-0 m-0">
<div class="h-full w-full overflow-y-scroll custom-scrollbar min-h-[600px]">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-8">
{#each tab.fields || [] as field}
<SettingFormField
key={field}
label={FIELD_LABELS[field] || field}
value={formData[field]}
onChange={(val) => handleFieldChange(field, val)}
/>
{/each}
</div>
</div>
</Tabs.Content>
{/each}
</Tabs.Root>
</div>
<style>
:global([data-radix-scroll-area-viewport]) {
scrollbar-width: none;
-ms-overflow-style: none;
}
:global([data-radix-scroll-area-viewport]::-webkit-scrollbar) {
display: none;
}
</style>

View File

@@ -0,0 +1,35 @@
export const SETTINGS_METADATA: Record<string, string[]> = {
ssisgen: [
"consecutivo", "dta", "dtaexpo", "subempresa", "patharch", "patharchtransmision", "pathtransexpo", "pathrespuesta",
"patharchped", "patharchpedconsm", "pathgenimpotemp", "pathgenexpo", "actseguridad", "controldes", "diadesactual",
"diavencimiento", "mensajevenc", "fechades", "factoriva", "validasifra", "decimalespeso", "decimalescant",
"decimalesvalor", "calvalbasetcped", "calvalbasetcpedexpo", "filtrocantidad", "muestraarchcodbarras", "datoshist",
"tipovenro", "cantvenro", "costoplanta", "firmapacking", "advertenciatm", "tomarsaldosvenc", "costoimpofijo",
"valparteexiste", "valmanifusado", "temporalfechapago", "asignadiasantdesc", "diasantdesc", "deshabilitardescparte",
"deshabilitardescparteing", "asignafracameparte", "validadecencant", "usartranspamedocame", "mostraradvertenciaro",
"escondaamexpacking", "calcdutypacking", "parammultiples", "fraccnivelpais", "covefechaemision", "valordllstcfacturaexpo",
"interfaceaaconsolidada", "interfaceaatcfpff", "incluirobscoveobsimpo", "agregarincreimpo", "componentebom",
"limitesubensamble", "actpdfreportes", "patharchpdfimpo", "patharchpdfexpo", "noimprimircons", "mostraradvertenciarovalor",
"partesypedimentosporcliente", "mensajesvurfc", "mostrarpackinglistingles", "omitirempaqueencodigobarras", "restringepaisimpo",
"bloqueoaldesactivarnumerodeparte", "restringpaisexpo", "geninformeanexo31", "utilizarfechapagopeddeundiaanterior",
"utilizarequivalenciasdeumpornumerodeparte", "utilizartitulosalternativosimpresionfactura", "usarfactorconversionpornumerodeparte",
"usarvude128o256", "usartcdelafechapagopedimpoendescarga", "solicitarcontrasenaadministrador", "agregarnumeroembarque",
"utilizarumdeexistenciaentransmisionvu", "utilizarcodigodebrokerdeclienteenmainx30", "hojacalculosepararincrementablesanexo3",
"hojacalculodesglosefacturaanexo3", "usarvaloragregadoenfacturaamericana", "ocultarinformacionfraccion",
"resaltarsaldostempconcolor", "valoragregadoenfacturamexicana", "validarsectorprosecr8", "agregarsubtotalinterfazaa"
],
ssismex: [
"consecutivo", "prefijocm", "consecutivocm", "porparteclasemex", "porparteclaseame", "proveedor", "vendidoconsignado",
"vendidoa", "enviadotransferido", "enviadoa", "flete", "paisorigenmex", "numpartemex", "firmafmex", "fraccionimp",
"tipofraccmex", "tasafraccmex", "umequivalentemex", "numparteame", "fraccioname", "paisorigename", "umequivalenteame",
"firmafame", "impordencomp", "decimalespeso", "decimalescant", "decimalesvalor", "decimalescosto", "tipomoneda",
"clavemoneda", "transportista", "conductor", "transporte", "numtransporte", "observacione", "observacioni",
"leyendamex", "leyendaame", "firmaaamex", "firmapmex", "firmaaaame", "firmapame", "firmaeamex", "firmaeame",
"firmasamex", "firmasame", "claveregimen", "claveregimename", "numfactura", "fechafactura", "numeropedimento",
"fechapedimento", "pedimentomex", "clientemex", "claveregmexo", "claveregmexd", "usarvalorameric", "consecutivoas",
"consecutivops", "usatranspfactu", "ocultarfechahora"
],
qsisgen: [
"consecutivo", "actvaloragre", "valoragregadogen"
]
};

View File

@@ -518,6 +518,10 @@ export function getSidebarData(): SidebarData {
title: m["sidebar.reference_data.usuarios"](),
url: "",
},
{
title: "General",
url: "/dashboard/settings/general",
},
],
},
],

View File

@@ -0,0 +1,171 @@
<script lang="ts">
import { appSettingsApi } from '$lib/api/dashboard/a76/app-settings';
import { toast } from 'svelte-sonner';
import {
Settings2, Save, RefreshCw, Layers,
Database, Globe, Search, Info, HelpCircle,
ArrowRight
} from 'lucide-svelte';
import { fade, slide, fly } from 'svelte/transition';
import { authStore } from '$lib/auth';
import { companyStore } from '$lib/stores/company.svelte';
// Componentes UI
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
// Custom Components
import SettingHierarchyBadge from '$lib/components/dashboard/settings/SettingHierarchyBadge.svelte';
import SsisGenTabsForm from '$lib/components/dashboard/settings/SsisGenTabsForm.svelte';
import { currentUser } from '$lib/auth';
let tenantIdVal = $derived(companyStore.activeCompany?.tenant_id || 0);
let companyIdVal = $derived(companyStore.activeCompany?.id || 0);
let level = $state<'global' | 'tenant' | 'company'>('company');
let activeCategory = $state('ssisgen');
let resolvedSettings = $state<any>(null);
let loading = $state(false);
let saving = $state(false);
// Form internal state tracking
let currentFormData = $state<any>({});
// Load when company changes or on mount
$effect(() => {
if (companyIdVal) {
handleResolve();
}
});
async function handleSave() {
if (!companyIdVal || !tenantIdVal) {
toast.error('No hay contexto de empresa o tenant activo');
return;
}
saving = true;
try {
const payload = {
tenant_id: tenantIdVal,
company_id: companyIdVal,
settings: {
[activeCategory]: currentFormData
}
};
await appSettingsApi.upsert(payload);
toast.success(`Configuraciones de ${activeCategory.toUpperCase()} guardadas correctamente`);
await handleResolve();
} catch (e: any) {
toast.error('Error al guardar: ' + e.message);
} finally {
saving = false;
}
}
async function handleResolve() {
if (!companyIdVal || !tenantIdVal) return;
loading = true;
try {
resolvedSettings = await appSettingsApi.getResolved(tenantIdVal, companyIdVal);
if (resolvedSettings && resolvedSettings[activeCategory]) {
currentFormData = { ...resolvedSettings[activeCategory] };
} else {
currentFormData = {};
}
} catch (e: any) {
toast.error('Error al cargar configuraciones: ' + e.message);
} finally {
loading = false;
}
}
function handleFormUpdate(newData: any) {
currentFormData = newData;
}
</script>
<svelte:head>
<title>Configuración General | Anexo 76</title>
</svelte:head>
<div class="flex flex-col h-[calc(100vh-64px)] overflow-hidden bg-[#050505] text-white">
<!-- Main Header -->
<header class="h-20 shrink-0 px-8 border-b border-white/5 bg-black/40 backdrop-blur-md flex items-center justify-between z-10">
<div class="flex items-center gap-4">
<div class="p-2.5 bg-primary/20 rounded-xl border border-primary/20 shadow-lg shadow-primary/5">
<Settings2 class="w-6 h-6 text-primary" />
</div>
<div class="flex flex-col">
<h1 class="text-xl font-bold tracking-tight flex items-center gap-2">
Configuraciones Generales
<span class="text-[10px] font-medium bg-white/5 px-2 py-0.5 rounded-full border border-white/10 text-white/40">SsisGen</span>
</h1>
<div class="flex items-center gap-2 mt-0.5">
<div class="flex items-center gap-1.5 text-xs text-white/30">
<Database class="w-3 h-3" />
<span>Tenant: {tenantIdVal}</span>
</div>
<div class="w-1 h-1 rounded-full bg-white/10"></div>
<div class="flex items-center gap-1.5 text-xs text-white/30">
<Layers class="w-3 h-3" />
<span>Empresa: {companyIdVal}</span>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-4">
{#if loading}
<div in:fade class="flex items-center gap-2 text-xs text-white/40 bg-white/5 px-3 py-1.5 rounded-lg border border-white/5">
<RefreshCw class="w-3 h-3 animate-spin" />
Cargando...
</div>
{/if}
<Button variant="default" onclick={handleSave} disabled={saving || loading} class="h-11 px-6 bg-primary hover:bg-primary/90 text-white shadow-xl shadow-primary/20 font-bold transition-all hover:scale-[1.02] active:scale-[0.98]">
{#if saving}
<RefreshCw class="w-4 h-4 mr-2 animate-spin" />
Guardando...
{:else}
<Save class="w-4 h-4 mr-2" />
Guardar Cambios
{/if}
</Button>
</div>
</header>
<!-- Workspace Area -->
<main class="flex-1 overflow-hidden p-6 bg-gradient-to-b from-black/20 to-transparent">
<div class="h-full flex flex-col max-w-[1600px] mx-auto w-full relative">
<!-- Form Background Glow -->
<div class="absolute -top-[10%] -left-[10%] w-[40%] h-[40%] bg-primary/5 blur-[120px] rounded-full pointer-events-none"></div>
<div class="absolute -bottom-[10%] -right-[10%] w-[40%] h-[40%] bg-purple-500/5 blur-[120px] rounded-full pointer-events-none"></div>
{#if loading && !resolvedSettings}
<div class="flex-1 flex flex-col items-center justify-center gap-4">
<div class="p-4 bg-white/5 rounded-2xl border border-white/10">
<RefreshCw class="w-10 h-10 animate-spin text-primary" />
</div>
<p class="text-xs font-bold uppercase tracking-[0.2em] text-white/20">Inicializando Parámetros</p>
</div>
{:else}
<div in:fade={{ duration: 400 }} class="flex-1 h-full shadow-2xl">
<SsisGenTabsForm
currentData={currentFormData}
onUpdate={handleFormUpdate}
/>
</div>
{/if}
</div>
</main>
</div>
<style>
:global(body) { overflow: hidden; }
</style>

View File

@@ -0,0 +1 @@
export const ssr = false;