Enmascaramiento y regax para datos sensibles en los agentes aduanales
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{mode === 'create' ? 'Nuevo Agente Aduanal' : 'Editar Agente Aduanal'}
|
||||
@@ -148,7 +198,7 @@
|
||||
|
||||
<div class="grid gap-6 py-4">
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Identificación</h4>
|
||||
<h4 class="text-sm leading-none font-medium text-muted-foreground">Identificación</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave Agente *</Label>
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 col-span-2">
|
||||
<div class="col-span-2 space-y-2">
|
||||
<Label for="name">Nombre / Razón Social</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -229,16 +280,48 @@
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
value={formData.tax_id}
|
||||
oninput={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="personal_id">CURP</Label>
|
||||
<Input
|
||||
id="personal_id"
|
||||
value={formData.personal_id}
|
||||
oninput={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-2 space-y-2">
|
||||
<Label for="contact">Nombre Contacto</Label>
|
||||
<Input
|
||||
id="contact"
|
||||
bind:value={formData.contact}
|
||||
value={formData.contact}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Contacto</h4>
|
||||
<h4 class="text-sm leading-none font-medium text-muted-foreground">Contacto</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2 col-span-1">
|
||||
<div class="col-span-1 space-y-2">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input id="phone" bind:value={formData.phone} disabled={loading} />
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone}
|
||||
oninput={(e) => {
|
||||
formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, '');
|
||||
e.currentTarget.value = formData.phone;
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<div class="col-span-1 space-y-2">
|
||||
<Label for="fax">Fax</Label>
|
||||
<Input
|
||||
id="fax"
|
||||
value={formData.fax}
|
||||
oninput={(e) => {
|
||||
formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, '');
|
||||
e.currentTarget.value = formData.fax;
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-1 space-y-2">
|
||||
<Label for="email">Correo Electrónico</Label>
|
||||
<Input id="email" type="email" bind:value={formData.email} disabled={loading} />
|
||||
</div>
|
||||
@@ -265,7 +368,7 @@
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Dirección Fiscal</h4>
|
||||
<h4 class="text-sm leading-none font-medium text-muted-foreground">Dirección Fiscal</h4>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Calle y Número</Label>
|
||||
@@ -275,9 +378,16 @@
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">C.P.</Label>
|
||||
<Input id="postal_code" bind:value={formData.postal_code} disabled={loading} oninput={(e) => { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} />
|
||||
<Input
|
||||
id="postal_code"
|
||||
bind:value={formData.postal_code}
|
||||
disabled={loading}
|
||||
oninput={(e) => {
|
||||
formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<div class="col-span-2 space-y-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input id="city" bind:value={formData.city} disabled={loading} />
|
||||
</div>
|
||||
|
||||
@@ -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 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Editar Agente Aduanal</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
@@ -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 @@
|
||||
<Label for="edit-phone">Teléfono</Label>
|
||||
<Input
|
||||
id="edit-phone"
|
||||
bind:value={formData.phone}
|
||||
value={formData.phone}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
<Label for="edit-fax">Fax</Label>
|
||||
<Input
|
||||
id="edit-fax"
|
||||
bind:value={formData.fax}
|
||||
value={formData.fax}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
<Label for="edit-contact">Contacto</Label>
|
||||
<Input
|
||||
id="edit-contact"
|
||||
bind:value={formData.contact}
|
||||
value={formData.contact}
|
||||
oninput={(e) => {
|
||||
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, '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -366,9 +418,16 @@
|
||||
<Label for="edit-tax_id">RFC</Label>
|
||||
<Input
|
||||
id="edit-tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
value={formData.tax_id}
|
||||
oninput={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
@@ -377,9 +436,16 @@
|
||||
<Label for="edit-personal_id">CURP</Label>
|
||||
<Input
|
||||
id="edit-personal_id"
|
||||
bind:value={formData.personal_id}
|
||||
value={formData.personal_id}
|
||||
oninput={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>RFC</Label>
|
||||
<Input
|
||||
bind:value={formData.tax_id}
|
||||
value={formData.tax_id}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>CURP</Label>
|
||||
<Input
|
||||
bind:value={formData.personal_id}
|
||||
value={formData.personal_id}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>Persona de Contacto</Label>
|
||||
<Input
|
||||
bind:value={formData.contact}
|
||||
value={formData.contact}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>Teléfono</Label>
|
||||
<Input
|
||||
bind:value={formData.phone}
|
||||
value={formData.phone}
|
||||
oninput={(e) => {
|
||||
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 @@
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Fax</Label>
|
||||
<Input bind:value={formData.fax} disabled={loading} class="h-10" />
|
||||
<Input
|
||||
value={formData.fax}
|
||||
oninput={(e) => {
|
||||
formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, '');
|
||||
e.currentTarget.value = formData.fax;
|
||||
}}
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Correo Electrónico</Label>
|
||||
@@ -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, '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2 md:col-span-2">
|
||||
|
||||
Reference in New Issue
Block a user