From a77761f2e51edb7702716ab47d1f0d000449d3e3 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Wed, 4 Mar 2026 10:41:37 -0600 Subject: [PATCH 1/5] Enmascaramiento y regax para datos sensibles en los agentes aduanales --- .../api/v1/modules/a76/customs_brokers/dto.py | 32 ++-- .../modules/a76/customs_brokers/services.py | 19 ++- backend/core/error_handlers.py | 112 +++++++++++++- backend/main.py | 25 --- .../customs_brokers/create-dialog.svelte | 144 +++++++++++++++--- .../customs_brokers/edit-dialog.svelte | 86 +++++++++-- .../customs_brokers/edit/[[id]]/+page.svelte | 85 ++++++++++- 7 files changed, 420 insertions(+), 83 deletions(-) diff --git a/backend/api/v1/modules/a76/customs_brokers/dto.py b/backend/api/v1/modules/a76/customs_brokers/dto.py index 9f1309c7..64bbf65c 100644 --- a/backend/api/v1/modules/a76/customs_brokers/dto.py +++ b/backend/api/v1/modules/a76/customs_brokers/dto.py @@ -12,16 +12,16 @@ class CustomsBrokerBaseDTO(BaseModel): postal_code: Optional[str] = None city: Optional[str] = None state: Optional[str] = None - phone: Optional[str] = None + phone: Optional[str] = Field(None, pattern=r"^$|^[\d\s\-\+\(\)]+$") fax: Optional[str] = None - email: Optional[str] = None + email: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") country: Optional[str] = None - tax_id: Optional[str] = None - personal_id: Optional[str] = None + tax_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$") + personal_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$") position: Optional[str] = None - license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$") + license: Optional[str] = Field(None, max_length=4, pattern=r"^$|^[0-9]*[1-9][0-9]*$") company: Optional[str] = None - contact: Optional[str] = None + contact: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]+$") class CustomsBrokerCreateDTO(CustomsBrokerBaseDTO): @@ -58,16 +58,16 @@ class CustomsBrokerDTO(BaseModel): postal_code: Optional[str] = None city: Optional[str] = None state: Optional[str] = None - phone: Optional[str] = None + phone: Optional[str] = Field(None, pattern=r"^$|^[\d\s\-\+\(\)]+$") fax: Optional[str] = None - email: Optional[str] = None + email: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") country: Optional[str] = None - tax_id: Optional[str] = None - personal_id: Optional[str] = None + tax_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$") + personal_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$") position: Optional[str] = None - license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$") + license: Optional[str] = Field(None, max_length=4, pattern=r"^$|^[0-9]*[1-9][0-9]*$") company: Optional[str] = None - contact: Optional[str] = None + contact: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]+$") tenant_id: str company_id: str @@ -109,14 +109,14 @@ class CustomsBrokerPersonnelDTO(BaseModel): broker_key: str = Field(..., max_length=5, pattern=r"^[a-zA-Z0-9]+$") line: int name: Optional[str] = None - tax_id: Optional[str] = None - personal_id: Optional[str] = None + tax_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$") + personal_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$") position: Optional[str] = None - license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$") + license: Optional[str] = Field(None, max_length=4, pattern=r"^$|^[0-9]*[1-9][0-9]*$") first_name: Optional[str] = None last_name: Optional[str] = None middle_name: Optional[str] = None - email: Optional[str] = None + email: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") tenant_id: Optional[int] = None company_id: Optional[int] = None diff --git a/backend/api/v1/modules/a76/customs_brokers/services.py b/backend/api/v1/modules/a76/customs_brokers/services.py index dc00f352..01cb13e2 100644 --- a/backend/api/v1/modules/a76/customs_brokers/services.py +++ b/backend/api/v1/modules/a76/customs_brokers/services.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError from . import dto, models @@ -45,9 +46,13 @@ class CustomsBrokerService: new_broker = models.CustomsBroker(**broker_dict) db.add(new_broker) - db.commit() - db.refresh(new_broker) - return new_broker + try: + db.commit() + db.refresh(new_broker) + return new_broker + except IntegrityError: + db.rollback() + raise ValueError("La clave del agente ya existe o hay datos duplicados.") @staticmethod def update(db: Session, broker_key: str, tenant_id: int, broker_data: dto.CustomsBrokerUpdateDTO, company_id: int): @@ -56,8 +61,12 @@ class CustomsBrokerService: if broker: for key, value in broker_data.model_dump(exclude_unset=True).items(): setattr(broker, key, value) - db.commit() - db.refresh(broker) + try: + db.commit() + db.refresh(broker) + except IntegrityError: + db.rollback() + raise ValueError("Los datos duplicados no pueden ser guardados o hay un conflicto de integridad.") return broker @staticmethod diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index c3c4c90c..3cbd397c 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -8,7 +8,9 @@ from typing import Any, Dict from fastapi import Request, status, HTTPException from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder +from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError from sqlalchemy.exc import IntegrityError, SQLAlchemyError from .config import settings @@ -57,20 +59,69 @@ async def base_exception_handler( return response +# Mapa de campos técnicos a nombres legibles en español +_FIELD_LABELS: Dict[str, str] = { + "broker_key": "Clave del Agente", + "license": "Patente", + "tax_id": "RFC", + "personal_id": "CURP", + "email": "Correo Electrónico", + "phone": "Teléfono", + "fax": "Fax", + "contact": "Nombre de Contacto", + "name": "Nombre / Razón Social", + "address": "Dirección", + "postal_code": "Código Postal", + "city": "Ciudad", + "state": "Estado", + "country": "País", +} + +_FIELD_PATTERN_MESSAGES: Dict[str, str] = { + "broker_key": "La Clave del Agente solo puede contener letras y números (máx. 5 caracteres).", + "license": "La Patente debe ser un número entre 1 y 9999 (no puede ser 0 ni contener letras).", + "tax_id": "El RFC no tiene el formato correcto. Ejemplo válido: XAXX010101000.", + "personal_id": "La CURP no tiene el formato correcto. Debe tener 18 caracteres alfanuméricos.", + "email": "El correo electrónico no tiene un formato válido. Ejemplo: usuario@dominio.com.", + "phone": "El teléfono solo puede contener dígitos, espacios y los símbolos: +, -, (, ).", + "contact": "El nombre de contacto contiene caracteres no permitidos. Use solo letras, números y puntuación básica.", +} + + +def _friendly_message(field_key: str, error_type: str) -> str: + """Devuelve un mensaje de error legible en español según el campo y tipo de error.""" + if error_type in ("string_pattern_mismatch", "value_error"): + return _FIELD_PATTERN_MESSAGES.get( + field_key, + f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' contiene un valor con formato inválido.", + ) + if error_type == "string_too_long": + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' excede la longitud máxima permitida." + if error_type == "string_too_short": + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' es demasiado corto." + if error_type in ("missing", "value_error.missing"): + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' es obligatorio." + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' contiene un valor inválido." + + async def validation_exception_handler( request: Request, exc: RequestValidationError, ) -> JSONResponse: """ - Manejador para errores de validación de Pydantic/FastAPI + Manejador para errores de validación de Pydantic/FastAPI. + Devuelve mensajes legibles en español. """ errors = [] for error in exc.errors(): - field = ".".join(str(loc) for loc in error["loc"] if loc != "body") + loc_parts = [str(loc) for loc in error["loc"] if loc != "body"] + field = ".".join(loc_parts) + field_key = loc_parts[-1] if loc_parts else "" + errors.append( { "field": field, - "message": error["msg"], + "message": _friendly_message(field_key, error["type"]), "type": error["type"], } ) @@ -80,11 +131,63 @@ async def validation_exception_handler( extra={"errors": errors}, ) + summary = ( + errors[0]["message"] + if len(errors) == 1 + else f"Hay {len(errors)} errores de validación: " + " | ".join(e["message"] for e in errors) + ) + response = JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "error": "VALIDATION_ERROR", - "message": "Error de validación en los datos recibidos", + "message": summary, + "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, + "errors": errors, + }, + ) + for k, v in _cors_headers(request).items(): + response.headers[k] = v + return response + + +async def inner_validation_exception_handler( + request: Request, + exc: ValidationError, +) -> JSONResponse: + """ + Manejador para errores de validación de Pydantic lanzados internamente (como en tenant_crud_routes). + """ + errors = [] + for error in exc.errors(): + loc_parts = [str(loc) for loc in error["loc"] if loc != "body"] + field = ".".join(loc_parts) + field_key = loc_parts[-1] if loc_parts else "" + + errors.append( + { + "field": field, + "message": _friendly_message(field_key, error["type"]), + "type": error["type"], + } + ) + + logger.warning( + f"Inner Validation Error en {request.url.path}", + extra={"errors": errors}, + ) + + summary = ( + errors[0]["message"] + if len(errors) == 1 + else f"Hay {len(errors)} errores de validación: " + " | ".join(e["message"] for e in errors) + ) + + response = JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error": "VALIDATION_ERROR", + "message": summary, "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, "errors": errors, }, @@ -221,6 +324,7 @@ def register_exception_handlers(app) -> None: app.add_exception_handler(BaseAPIException, base_exception_handler) app.add_exception_handler(HTTPException, http_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(ValidationError, inner_validation_exception_handler) app.add_exception_handler(IntegrityError, integrity_error_handler) app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler) app.add_exception_handler(Exception, general_exception_handler) diff --git a/backend/main.py b/backend/main.py index b50f92a0..6d6e3ddb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -153,36 +153,11 @@ def _cors_headers_for_request(request: Request): return {} -# Add validation error handler -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request: Request, exc: RequestValidationError): - logger.error( - f"Validation error for {request.method} {request.url.path}: {exc.errors()}" - ) - logger.error(f"Request body: {await request.body()}") - response = JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": exc.errors(), "body": exc.body}, - ) for k, v in _cors_headers_for_request(request).items(): response.headers[k] = v return response -# Add HTTP exception handler -@app.exception_handler(HTTPException) -async def http_exception_handler(request: Request, exc: HTTPException): - logger.error( - f"HTTP {exc.status_code} for {request.method} {request.url.path}: {exc.detail}" - ) - response = JSONResponse( - status_code=exc.status_code, - content={"detail": exc.detail}, - ) - for k, v in _cors_headers_for_request(request).items(): - response.headers[k] = v - return response - def run_migrations(): subprocess.run(["alembic", "upgrade", "head"], check=True) diff --git a/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte b/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte index 672f019a..c34de920 100644 --- a/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte +++ b/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte @@ -38,6 +38,7 @@ city: '', state: '', country: '', + personal_id: '', tenant_id: '', // Se llenará en el submit o por defecto company_id: '' }; @@ -62,6 +63,7 @@ contact: initialData.contact || '', address: initialData.address || '', postal_code: initialData.postal_code || '', + personal_id: initialData.personal_id || '', city: initialData.city || '', state: initialData.state || '', country: initialData.country || '', @@ -80,6 +82,7 @@ contact: '', address: '', postal_code: '', + personal_id: '', city: '', state: '', country: 'MEX', // Valor por defecto sugerido @@ -116,6 +119,48 @@ loading = false; return; } + if ( + formData.license === '0' || + formData.license === '0000' || + /^0+$/.test(formData.license) + ) { + toast.error('La Patente no puede ser 0'); + loading = false; + return; + } + if (formData.tax_id && !/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i.test(formData.tax_id)) { + toast.error('El formato del RFC es inválido'); + loading = false; + return; + } + if ( + formData.personal_id && + !/^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$/i.test( + formData.personal_id + ) + ) { + toast.error('El formato de la CURP es inválido'); + loading = false; + return; + } + if ( + formData.email && + !/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(formData.email) + ) { + toast.error('El formato del correo electrónico es inválido'); + loading = false; + return; + } + if (formData.phone && !/^[\d\s\-\+\(\)]+$/.test(formData.phone)) { + toast.error('El teléfono contiene caracteres no válidos'); + loading = false; + return; + } + if (formData.fax && !/^[\d\s\-\+\(\)]+$/.test(formData.fax)) { + toast.error('El fax contiene caracteres no válidos'); + loading = false; + return; + } // Inyectar company_id si no viene const payload = { ...formData, company_id: companyId }; @@ -125,9 +170,14 @@ toast.success( mode === 'create' ? 'Agente creado correctamente' : 'Agente actualizado correctamente' ); - } catch (error) { + } catch (error: unknown) { console.error(error); - toast.error('Error al guardar el agente aduanal'); + // Intentar extraer el mensaje del servidor si lo hay + let msg = 'Error al guardar el agente aduanal'; + if (error && typeof error === 'object' && 'message' in error) { + msg = (error as { message: string }).message || msg; + } + toast.error(msg); } finally { loading = false; } @@ -135,7 +185,7 @@ - + {mode === 'create' ? 'Nuevo Agente Aduanal' : 'Editar Agente Aduanal'} @@ -148,7 +198,7 @@
-

Identificación

+

Identificación

@@ -156,7 +206,7 @@ id="broker_key" value={formData.broker_key} oninput={(e) => { - const val = e.currentTarget.value.toUpperCase(); + const val = e.currentTarget.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); if (val.length > 5) { brokerKeyError = true; formData.broker_key = val.slice(0, 5); @@ -169,10 +219,11 @@ } else { brokerKeyError = false; formData.broker_key = val; + e.currentTarget.value = formData.broker_key; } }} placeholder="Ej. 550" - maxlength="6" + maxlength={6} class={brokerKeyError ? 'border-red-500 focus-visible:ring-red-500' : ''} disabled={mode === 'edit' || loading} /> @@ -204,7 +255,7 @@ } }} placeholder="Ej. 3421" - maxlength="5" + maxlength={5} class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''} disabled={loading} /> @@ -216,7 +267,7 @@
-
+
RFC { + formData.tax_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9&Ñ]/g, '') + .slice(0, 13); + e.currentTarget.value = formData.tax_id; + }} placeholder="RFC de la agencia" + maxlength={13} disabled={loading} />
+ + { + formData.personal_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .slice(0, 18); + e.currentTarget.value = formData.personal_id; + }} + placeholder="CURP (Opcional)" + maxlength={18} + disabled={loading} + /> +
+
{ + formData.contact = e.currentTarget.value.replace( + /[^a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]/g, + '' + ); + e.currentTarget.value = formData.contact; + }} placeholder="Persona de contacto" disabled={loading} /> @@ -249,13 +332,33 @@
-

Contacto

+

Contacto

-
+
- + { + formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.phone; + }} + disabled={loading} + />
-
+
+ + { + formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.fax; + }} + disabled={loading} + /> +
+
@@ -265,7 +368,7 @@
-

Dirección Fiscal

+

Dirección Fiscal

@@ -275,9 +378,16 @@
- { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} /> + { + formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); + }} + />
-
+
diff --git a/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte b/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte index 294c7515..fa98ecc3 100644 --- a/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte @@ -79,6 +79,41 @@ loading = true; error = null; + // Validaciones Básicas + if (formData.license === '0' || formData.license === '0000' || /^0+$/.test(formData.license)) { + error = 'La Patente no puede ser 0'; + loading = false; + return; + } + if (formData.tax_id && !/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i.test(formData.tax_id)) { + error = 'El formato del RFC es inválido'; + loading = false; + return; + } + if ( + formData.personal_id && + !/^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$/i.test( + formData.personal_id + ) + ) { + error = 'El formato de la CURP es inválido'; + loading = false; + return; + } + if ( + formData.email && + !/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(formData.email) + ) { + error = 'El formato del correo electrónico es inválido'; + loading = false; + return; + } + if (formData.phone && !/^[\d\s\-\+\(\)]+$/.test(formData.phone)) { + error = 'El teléfono contiene caracteres no válidos'; + loading = false; + return; + } + try { const payload: CreateCustomsBrokerData = { broker_key: broker.broker_key, // La clave no se edita @@ -138,7 +173,7 @@ - + Editar Agente Aduanal @@ -218,7 +253,7 @@ } }} placeholder="Número de patente" - maxlength="5" + maxlength={5} class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''} disabled={loading} /> @@ -251,7 +286,11 @@ { + formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.phone; + }} placeholder="Número telefónico" maxlength={30} disabled={loading} @@ -262,7 +301,11 @@ { + formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.fax; + }} placeholder="Número de fax" maxlength={30} disabled={loading} @@ -286,7 +329,14 @@ { + formData.contact = e.currentTarget.value.replace( + /[^a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]/g, + '' + ); + e.currentTarget.value = formData.contact; + }} placeholder="Nombre del contacto" maxlength={80} disabled={loading} @@ -318,7 +368,9 @@ placeholder="C.P." maxlength={15} disabled={loading} - oninput={(e) => { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} + oninput={(e) => { + formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); + }} />
@@ -366,9 +418,16 @@ { + formData.tax_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9&Ñ]/g, '') + .slice(0, 13); + e.currentTarget.value = formData.tax_id; + }} placeholder="RFC" - maxlength={30} + maxlength={13} disabled={loading} />
@@ -377,9 +436,16 @@ { + formData.personal_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .slice(0, 18); + e.currentTarget.value = formData.personal_id; + }} placeholder="CURP" - maxlength={20} + maxlength={18} disabled={loading} />
diff --git a/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte index c52512ae..0c001f08 100644 --- a/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte @@ -228,6 +228,44 @@ return; } + if (formData.license === '0' || formData.license === '0000' || /^0+$/.test(formData.license)) { + error = 'La Patente no puede ser 0'; + toast.error(error); + return; + } + + if (formData.tax_id && !/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i.test(formData.tax_id)) { + error = 'El formato del RFC es inválido'; + toast.error(error); + return; + } + + if ( + formData.personal_id && + !/^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$/i.test( + formData.personal_id + ) + ) { + error = 'El formato de la CURP es inválido'; + toast.error(error); + return; + } + + if ( + formData.email && + !/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(formData.email) + ) { + error = 'El formato del correo electrónico es inválido'; + toast.error(error); + return; + } + + if (formData.phone && !/^[\d\s\-\+\(\)]+$/.test(formData.phone)) { + error = 'El teléfono contiene caracteres no válidos'; + toast.error(error); + return; + } + loading = true; error = null; try { @@ -435,7 +473,14 @@
{ + formData.tax_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9&Ñ]/g, '') + .slice(0, 13); + e.currentTarget.value = formData.tax_id; + }} placeholder="RFC de la empresa" disabled={loading} class="h-10" @@ -444,7 +489,14 @@
{ + formData.personal_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .slice(0, 18); + e.currentTarget.value = formData.personal_id; + }} placeholder="CURP si aplica" disabled={loading} class="h-10" @@ -467,7 +519,14 @@
{ + formData.contact = e.currentTarget.value.replace( + /[^a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]/g, + '' + ); + e.currentTarget.value = formData.contact; + }} placeholder="Nombre del contacto" disabled={loading} class="h-10" @@ -490,7 +549,11 @@
{ + formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.phone; + }} placeholder="656-000-0000" disabled={loading} class="h-10" @@ -498,7 +561,15 @@
- + { + formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.fax; + }} + disabled={loading} + class="h-10" + />
@@ -540,7 +611,9 @@ placeholder="32000" disabled={loading} class="h-10" - oninput={(e) => { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} + oninput={(e) => { + formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); + }} />
From 7a288a973acd910ee28e91b2b2e9b9cb1df0ed25 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Wed, 4 Mar 2026 11:49:33 -0600 Subject: [PATCH 2/5] 2 --- backend/celerybeat-schedule | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/celerybeat-schedule diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule deleted file mode 100644 index 8cda996d90f19534ef5b500c264fc536e50391cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI(O=}ZD7zgl)i6*fP6eB1KB3^8etZ&$Zcu;}|S*&0M!9$nLW@?6Rc4c=~Odybx zT9|{-DTp7yv-kx(d-4N#^5CUVKY^gnbhb8!;!Dt?_8*vip54vNPJWqvNCJ8H;%=SI zFt*t!m{`r@OB1h}4P&2ez!m}!fB*y_009U<00Izz00bZafqzlpWIky`3jqf>7y+yU ztOKkA+y|%wr~{}2r~{}2r~{}2s00659VptnnoZ}CowLd1f{ly$e!XV*H!f^F@z-OC zx_qI>PCaqls=xsbO7Lk^YgZUnZi7M2ZZNjo^M{=IVuN?oyqIf zJ{sl}wZF~Nyyt?aqtZ}2=JRioia7GTQE6@SUjQ^?wf~ZH(V5eZ9MI7tmV>GKwH{AI z;Gez8ABiCLT0-`hDQEUDm47@g~$dgqty7R))*T&8C7*{Aum=|YvQoJQ;(P*pR>4iBYI`z~{@xa Date: Wed, 4 Mar 2026 15:12:49 -0700 Subject: [PATCH 3/5] Movidos botones de Actualizar Est y Desactualizar --- .../components/dashboard/invoices/columns.ts | 77 +++-- .../dashboard/invoices/data-table.svelte | 51 ++-- .../dashboard/invoices/delete-dialog.svelte | 82 ++++-- .../routes/dashboard/invoices/+page.svelte | 268 +++++++++++++----- 4 files changed, 340 insertions(+), 138 deletions(-) diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts index f79c403c..95d3e7e5 100644 --- a/frontend/src/lib/components/dashboard/invoices/columns.ts +++ b/frontend/src/lib/components/dashboard/invoices/columns.ts @@ -1,7 +1,6 @@ import type { ColumnDef } from "@tanstack/table-core"; -import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { renderSnippet } from "$lib/components/ui/data-table/index.js"; import { createRawSnippet } from "svelte"; -import DataTableActions from "./data-table-actions.svelte"; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; import { getInvoiceTypeColor } from "$lib/utils"; @@ -18,29 +17,71 @@ export function createColumns( onSuccess?: () => void ): ColumnDef[] { return [ - // 0. NUEVA COLUMNA: Checkbox visual (el estado real lo maneja la opacidad) + // Checkbox visual { id: "select", header: ({ table }) => { - return renderSnippet( - createRawSnippet(() => ({ - render: () => `
` - })) - ); + const isAllSelected = table.getIsAllPageRowsSelected(); + const isSomeSelected = table.getIsSomePageRowsSelected(); + + const selectAllSnippet = createRawSnippet<[ + { checked: boolean; indeterminate: boolean; onchange: (e: Event) => void } + ]>((getProps) => { + const { checked, indeterminate, onchange } = getProps(); + return { + render: () => `
+ +
`, + setup: (node) => { + const input = node.querySelector('input') as HTMLInputElement; + if (input) { + input.indeterminate = indeterminate; + input.addEventListener('change', onchange); + } + } + }; + }); + + return renderSnippet(selectAllSnippet, { + checked: isAllSelected, + indeterminate: isSomeSelected && !isAllSelected, + onchange: (e: Event) => { + table.toggleAllPageRowsSelected(!!(e.target as HTMLInputElement).checked); + } + }); }, cell: ({ row }) => { const isSelected = row.getIsSelected(); - const checkboxSnippet = createRawSnippet<[{ selected: boolean }]>((getProps) => { - const { selected } = getProps(); + const checkboxSnippet = createRawSnippet<[ + { selected: boolean; onchange: (e: Event) => void } + ]>((getProps) => { + const { selected, onchange } = getProps(); return { render: () => `
- -
` + +
`, + setup: (node) => { + const input = node.querySelector('input') as HTMLInputElement; + if (input) { + input.addEventListener('change', onchange); + } + } }; }); - return renderSnippet(checkboxSnippet, { selected: isSelected }); + return renderSnippet(checkboxSnippet, { + selected: isSelected, + onchange: (e: Event) => { + e.stopPropagation(); // Evitar que el clic en el checkbox dispare el rowClick + row.toggleSelected(!!(e.target as HTMLInputElement).checked); + } + }); }, enableSorting: false, enableHiding: false, @@ -298,16 +339,6 @@ export function createColumns( return renderSnippet(relDocSnippet, { relDoc }); } }, - // 2. MODIFICAMOS AQUÍ: Pasamos onDownload al componente - { - id: "actions", - cell: ({ row }) => { - return renderComponent(DataTableActions, { - invoice: row.original, - onSuccess - }); - } - } ]; } diff --git a/frontend/src/lib/components/dashboard/invoices/data-table.svelte b/frontend/src/lib/components/dashboard/invoices/data-table.svelte index d549b5a2..998c3994 100644 --- a/frontend/src/lib/components/dashboard/invoices/data-table.svelte +++ b/frontend/src/lib/components/dashboard/invoices/data-table.svelte @@ -1,11 +1,8 @@
-
+
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} @@ -95,17 +97,16 @@ {#each table.getRowModel().rows as row (row.id)} - onRowClick && onRowClick(row.original)} > {#each row.getVisibleCells() as cell (cell.id)} - + {/each} @@ -116,7 +117,7 @@ {/each} - + {#if hasMore} @@ -124,13 +125,13 @@
{#if loading}
-
- Cargando más... +
+ Cargando más...
{:else} -
- Desplázate para cargar más -
+
Desplázate para cargar más
{/if}
diff --git a/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte index c813bbae..900fb156 100644 --- a/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte @@ -1,44 +1,63 @@