Merge pull request 'fix/trasnportistas' (#385) from fix/trasnportistas into development
Reviewed-on: ADUANASOFT/anexo76#385
This commit is contained in:
@@ -3,6 +3,7 @@ Carga de conjuntos FK para validación de import CSV de transportistas.
|
|||||||
Clarion: GTransportista (ClaveTrans), GPaises (Pais_Ame), GEstados (Descripcion), relación Estado-País.
|
Clarion: GTransportista (ClaveTrans), GPaises (Pais_Ame), GEstados (Descripcion), relación Estado-País.
|
||||||
"""
|
"""
|
||||||
from typing import Set, Tuple, Optional
|
from typing import Set, Tuple, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from core.database import CoreSessionLocal
|
from core.database import CoreSessionLocal
|
||||||
@@ -13,6 +14,7 @@ logger = logging.getLogger(__name__)
|
|||||||
def load_transportistas_fk_sets(
|
def load_transportistas_fk_sets(
|
||||||
tenant_id: Optional[int] = None,
|
tenant_id: Optional[int] = None,
|
||||||
company_id: Optional[int] = None,
|
company_id: Optional[int] = None,
|
||||||
|
db: Optional[Session] = None,
|
||||||
) -> Tuple[
|
) -> Tuple[
|
||||||
Set[str],
|
Set[str],
|
||||||
Set[str],
|
Set[str],
|
||||||
@@ -32,50 +34,56 @@ def load_transportistas_fk_sets(
|
|||||||
state_descriptions_upper: Set[str] = set()
|
state_descriptions_upper: Set[str] = set()
|
||||||
state_country_set: Set[Tuple[str, str]] = set()
|
state_country_set: Set[Tuple[str, str]] = set()
|
||||||
|
|
||||||
try:
|
def _load(session: Session):
|
||||||
with CoreSessionLocal() as session:
|
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
from api.v1.modules.public.reference_data.countries.models import Country
|
||||||
from api.v1.modules.public.reference_data.countries.models import Country
|
from api.v1.modules.public.reference_data.states.models import State
|
||||||
from api.v1.modules.public.reference_data.states.models import State
|
|
||||||
|
|
||||||
if tenant_id is not None and company_id is not None:
|
if tenant_id is not None and company_id is not None:
|
||||||
for row in (
|
for row in (
|
||||||
session.query(Transporter.transporter_key)
|
session.query(Transporter.transporter_key)
|
||||||
.filter(
|
.filter(
|
||||||
Transporter.tenant_id == tenant_id,
|
Transporter.tenant_id == tenant_id,
|
||||||
Transporter.company_id == company_id,
|
Transporter.company_id == company_id,
|
||||||
)
|
|
||||||
.all()
|
|
||||||
):
|
|
||||||
if row[0] and (row[0] or "").strip():
|
|
||||||
existing_transporter_keys.add((row[0] or "").strip().upper())
|
|
||||||
|
|
||||||
for row in session.query(Country.ame_key).all():
|
|
||||||
if row[0]:
|
|
||||||
valid_country_ame.add((row[0] or "").strip().upper())
|
|
||||||
|
|
||||||
for state in session.query(State).all():
|
|
||||||
country = (
|
|
||||||
session.query(Country)
|
|
||||||
.filter(Country.m3_key == state.m3_key)
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
ame = None
|
.all()
|
||||||
if country and (country.ame_key or "").strip():
|
):
|
||||||
ame = (country.ame_key or "").strip().upper()
|
if row[0] and (row[0] or "").strip():
|
||||||
|
existing_transporter_keys.add((row[0] or "").strip().upper())
|
||||||
|
|
||||||
desc = (state.description or "").strip()
|
for row in session.query(Country.ame_key).all():
|
||||||
if desc:
|
if row[0]:
|
||||||
state_descriptions_upper.add(desc.upper())
|
valid_country_ame.add((row[0] or "").strip().upper())
|
||||||
if ame:
|
|
||||||
state_country_set.add((ame, desc.upper()))
|
|
||||||
|
|
||||||
mex_key = (state.mex_key or "").strip()
|
for state in session.query(State).all():
|
||||||
if mex_key:
|
country = (
|
||||||
mk = mex_key.upper()
|
session.query(Country)
|
||||||
state_descriptions_upper.add(mk)
|
.filter(Country.m3_key == state.m3_key)
|
||||||
if ame:
|
.first()
|
||||||
state_country_set.add((ame, mk))
|
)
|
||||||
|
ame = None
|
||||||
|
if country and (country.ame_key or "").strip():
|
||||||
|
ame = (country.ame_key or "").strip().upper()
|
||||||
|
|
||||||
|
desc = (state.description or "").strip()
|
||||||
|
if desc:
|
||||||
|
state_descriptions_upper.add(desc.upper())
|
||||||
|
if ame:
|
||||||
|
state_country_set.add((ame, desc.upper()))
|
||||||
|
|
||||||
|
mex_key = (state.mex_key or "").strip()
|
||||||
|
if mex_key:
|
||||||
|
mk = mex_key.upper()
|
||||||
|
state_descriptions_upper.add(mk)
|
||||||
|
if ame:
|
||||||
|
state_country_set.add((ame, mk))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if db:
|
||||||
|
_load(db)
|
||||||
|
else:
|
||||||
|
with CoreSessionLocal() as session:
|
||||||
|
_load(session)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Transportistas import: could not load FK sets: %s", e)
|
logger.warning("Transportistas import: could not load FK sets: %s", e)
|
||||||
|
|||||||
@@ -37,24 +37,31 @@ def validate_row_transporter(
|
|||||||
clave = (row.get("CLAVE TRANSPORTISTA") or "").strip().upper()
|
clave = (row.get("CLAVE TRANSPORTISTA") or "").strip().upper()
|
||||||
use_partial = actualizar and bool(clave and clave in existing)
|
use_partial = actualizar and bool(clave and clave in existing)
|
||||||
|
|
||||||
|
if not use_partial and clave and clave in existing:
|
||||||
|
errors.append({
|
||||||
|
"line": line_num,
|
||||||
|
"col": "CLAVE TRANSPORTISTA",
|
||||||
|
"msg": f"La clave '{row.get('CLAVE TRANSPORTISTA')}' ya existe en el catálogo."
|
||||||
|
})
|
||||||
|
|
||||||
if use_partial:
|
if use_partial:
|
||||||
errors.extend(
|
errors.extend(
|
||||||
valida_parcial_transportistas(
|
valida_parcial_transportistas(
|
||||||
row,
|
row,
|
||||||
line_num,
|
line_num,
|
||||||
valid_country_ame=valid_country_ame,
|
valid_country_ame=valid_country_ame,
|
||||||
state_descriptions_upper=state_descriptions_upper,
|
state_descriptions_upper=state_descriptions_upper,
|
||||||
state_country_set=state_country_set,
|
state_country_set=state_country_set,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
errors.extend(
|
errors.extend(
|
||||||
valida_toda_transportistas(
|
valida_toda_transportistas(
|
||||||
row,
|
row,
|
||||||
line_num,
|
line_num,
|
||||||
valid_country_ame=valid_country_ame,
|
valid_country_ame=valid_country_ame,
|
||||||
state_descriptions_upper=state_descriptions_upper,
|
state_descriptions_upper=state_descriptions_upper,
|
||||||
state_country_set=state_country_set,
|
state_country_set=state_country_set,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ def transporter_model_to_row(t) -> Dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def validate_transporter_row_for_api(
|
def validate_transporter_row_for_api(
|
||||||
|
db: Session,
|
||||||
tenant_id: int,
|
tenant_id: int,
|
||||||
company_id: int,
|
company_id: int,
|
||||||
row: Dict[str, Any],
|
row: Dict[str, Any],
|
||||||
@@ -292,7 +293,7 @@ def validate_transporter_row_for_api(
|
|||||||
valid_country_ame,
|
valid_country_ame,
|
||||||
state_descriptions_upper,
|
state_descriptions_upper,
|
||||||
state_country_set,
|
state_country_set,
|
||||||
) = load_transportistas_fk_sets(tenant_id, company_id)
|
) = load_transportistas_fk_sets(tenant_id, company_id, db=db)
|
||||||
|
|
||||||
clave = (row.get("CLAVE TRANSPORTISTA") or "").strip().upper()
|
clave = (row.get("CLAVE TRANSPORTISTA") or "").strip().upper()
|
||||||
# existing set from loader is uppercased keys for this company
|
# existing set from loader is uppercased keys for this company
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class TransporterService:
|
|||||||
"""Create a new transporter"""
|
"""Create a new transporter"""
|
||||||
data = transporter_data.model_dump()
|
data = transporter_data.model_dump()
|
||||||
validate_transporter_row_for_api(
|
validate_transporter_row_for_api(
|
||||||
|
db,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
company_id,
|
company_id,
|
||||||
transporter_fields_to_csv_row(data),
|
transporter_fields_to_csv_row(data),
|
||||||
@@ -163,6 +164,7 @@ class TransporterService:
|
|||||||
}
|
}
|
||||||
merged.update(update_data)
|
merged.update(update_data)
|
||||||
validate_transporter_row_for_api(
|
validate_transporter_row_for_api(
|
||||||
|
db,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
company_id,
|
company_id,
|
||||||
transporter_fields_to_csv_row(merged),
|
transporter_fields_to_csv_row(merged),
|
||||||
|
|||||||
@@ -331,18 +331,48 @@ async function fetchApi<T = any>(
|
|||||||
// Manejo especial para errores 422 (validation error)
|
// Manejo especial para errores 422 (validation error)
|
||||||
if (response.status === 422) {
|
if (response.status === 422) {
|
||||||
// HTTPException(detail={ message, errors }) — catálogo / CSV parity
|
// HTTPException(detail={ message, errors }) — catálogo / CSV parity
|
||||||
const det = data.detail;
|
const det = data.detail || (typeof data.message === 'object' ? data.message : null);
|
||||||
const validationErrors = (errors: unknown[]) => errors as NonNullable<ApiResponse['validationErrors']>;
|
|
||||||
if (
|
if (
|
||||||
det &&
|
det &&
|
||||||
typeof det === 'object' &&
|
typeof det === "object" &&
|
||||||
!Array.isArray(det) &&
|
!Array.isArray(det) &&
|
||||||
Array.isArray((det as { errors?: unknown }).errors)
|
Array.isArray((det as { errors?: unknown }).errors)
|
||||||
) {
|
) {
|
||||||
const d = det as { message?: string; errors: unknown[] };
|
const d = det as {
|
||||||
|
message?: string;
|
||||||
|
errors: Array<{ col?: string; msg?: string; field?: string; message?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping for catalog column names to DTO field names
|
||||||
|
const colToField: Record<string, string> = {
|
||||||
|
"CLAVE TRANSPORTISTA": "transporter_key",
|
||||||
|
NOMBRE: "name",
|
||||||
|
"NOMBRE CORTO": "short_name",
|
||||||
|
RESPONSABLE: "responsible",
|
||||||
|
RFC: "rfc",
|
||||||
|
CALLES: "streets",
|
||||||
|
"CODIGO POSTAL": "postal_code",
|
||||||
|
CIUDAD: "city",
|
||||||
|
ESTADO: "state",
|
||||||
|
PAIS: "country",
|
||||||
|
"CODIGO CARGADOR": "loader_code",
|
||||||
|
"CODIGO CAAT": "caat_code",
|
||||||
|
"CODIGO TRANS": "transport_code",
|
||||||
|
"TIPO INTERFASE TRANS": "transport_interface_type",
|
||||||
|
"SERVIDOR FTP": "ftp_server",
|
||||||
|
"USUARIO FTP": "ftp_user",
|
||||||
|
"CLAVE ACCESO FTP": "ftp_password",
|
||||||
|
"DIRECTORIO FTP": "ftp_directory"
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizedErrors = d.errors.map((err) => ({
|
||||||
|
field: err.field || (err.col ? colToField[err.col] || err.col : ""),
|
||||||
|
message: err.message || err.msg || "Error de validación"
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: d.message || 'Error de validación',
|
error: d.message || (typeof data.message === 'string' ? data.message : 'Error de validación'),
|
||||||
validationErrors: validationErrors(d.errors),
|
validationErrors: normalizedErrors,
|
||||||
status: response.status
|
status: response.status
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -350,21 +380,31 @@ async function fetchApi<T = any>(
|
|||||||
if (data.errors && Array.isArray(data.errors)) {
|
if (data.errors && Array.isArray(data.errors)) {
|
||||||
return {
|
return {
|
||||||
error: data.message || 'Error de validación',
|
error: data.message || 'Error de validación',
|
||||||
validationErrors: validationErrors(data.errors),
|
validationErrors: data.errors as NonNullable<ApiResponse['validationErrors']>,
|
||||||
status: response.status
|
status: response.status
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Errores de validación de FastAPI (con detail)
|
// Errores de validación de FastAPI (con detail)
|
||||||
else if (data.detail) {
|
else if (data.detail) {
|
||||||
let errorMessage = 'Error de validación: ';
|
let errorMessage = 'Error de validación: ';
|
||||||
|
const vErrors: NonNullable<ApiResponse['validationErrors']> = [];
|
||||||
|
|
||||||
// FastAPI devuelve errores de validación en data.detail como array
|
// FastAPI devuelve errores de validación en data.detail como array
|
||||||
if (Array.isArray(data.detail)) {
|
if (Array.isArray(data.detail)) {
|
||||||
const errors = data.detail.map((err: any) => {
|
data.detail.forEach((err: any) => {
|
||||||
|
const fieldPath = err.loc ? err.loc.filter((l: any) => l !== 'body').join('.') : 'campo';
|
||||||
|
const msg = humanizeValidationMessage(err.msg || 'error de validación');
|
||||||
|
|
||||||
|
vErrors.push({
|
||||||
|
field: err.loc ? String(err.loc[err.loc.length - 1]) : 'campo',
|
||||||
|
message: msg
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
errorMessage += data.detail.map((err: any) => {
|
||||||
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
|
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
|
||||||
return `${field}: ${err.msg}`;
|
return `${field}: ${err.msg}`;
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
errorMessage += errors;
|
|
||||||
} else if (typeof data.detail === 'string') {
|
} else if (typeof data.detail === 'string') {
|
||||||
errorMessage = data.detail;
|
errorMessage = data.detail;
|
||||||
} else {
|
} else {
|
||||||
@@ -373,6 +413,7 @@ async function fetchApi<T = any>(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
|
validationErrors: vErrors.length ? vErrors : undefined,
|
||||||
status: response.status
|
status: response.status
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,7 +288,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
<Dialog.Root bind:open>
|
||||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-3xl">
|
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-3xl" onInteractOutside={(e) => e.preventDefault()}>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>{title}</Dialog.Title>
|
<Dialog.Title>{title}</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
<Dialog.Root bind:open>
|
||||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl">
|
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl" onInteractOutside={(e) => e.preventDefault()}>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>{title}</Dialog.Title>
|
<Dialog.Title>{title}</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
|
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
let fieldErrors = $state<Record<string, string>>({});
|
||||||
let countries = $state<Country[]>([]);
|
let countries = $state<Country[]>([]);
|
||||||
let states = $state<State[]>([]);
|
let states = $state<State[]>([]);
|
||||||
let refsLoading = $state(false);
|
let refsLoading = $state(false);
|
||||||
@@ -109,6 +110,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
error = null;
|
error = null;
|
||||||
|
fieldErrors = {};
|
||||||
loading = false;
|
loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -120,9 +122,89 @@
|
|||||||
void loadReferenceData();
|
void loadReferenceData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function validateForm(): boolean {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Clave del transportista
|
||||||
|
if (!formData.transporter_key?.trim()) {
|
||||||
|
errors.transporter_key = 'La clave es obligatoria';
|
||||||
|
} else if (/\s/.test(formData.transporter_key)) {
|
||||||
|
errors.transporter_key = 'La clave no puede contener espacios';
|
||||||
|
} else if (!/^[A-Za-z0-9_-]+$/.test(formData.transporter_key)) {
|
||||||
|
errors.transporter_key = 'La clave solo permite letras, números, guiones y guiones bajos';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nombre / Razón Social
|
||||||
|
if (!formData.name?.trim()) {
|
||||||
|
errors.name = 'El nombre o razón social es obligatorio';
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC (Opcional, pero si se pone debe ser válido si es MX)
|
||||||
|
if (formData.rfc?.trim()) {
|
||||||
|
const rfcRegex =
|
||||||
|
/^([A-ZÑ&]{3,4}) ?(?:- ?)?(\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])) ?(?:- ?)?([A-Z\d]{2})([A-Z\d])$/i;
|
||||||
|
if (formData.country === 'MEX' && !rfcRegex.test(formData.rfc.trim())) {
|
||||||
|
errors.rfc = 'Formato de RFC inválido para México';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Código Postal
|
||||||
|
if (formData.postal_code?.trim()) {
|
||||||
|
if (formData.country === 'MEX' && !/^\d{5}$/.test(formData.postal_code.trim())) {
|
||||||
|
errors.postal_code = 'El código postal en México debe ser de 5 dígitos';
|
||||||
|
} else if (!/^\d+$/.test(formData.postal_code.trim())) {
|
||||||
|
errors.postal_code = 'El código postal debe ser numérico';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Códigos de transporte
|
||||||
|
if (formData.caat_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.caat_code)) {
|
||||||
|
errors.caat_code = 'El código CAAT debe ser alfanumérico';
|
||||||
|
}
|
||||||
|
if (formData.transport_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.transport_code)) {
|
||||||
|
errors.transport_code = 'El código de transporte debe ser alfanumérico';
|
||||||
|
}
|
||||||
|
if (formData.loader_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.loader_code)) {
|
||||||
|
errors.loader_code = 'El código de cargador debe ser alfanumérico';
|
||||||
|
}
|
||||||
|
if (formData.filler_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.filler_code)) {
|
||||||
|
errors.filler_code = 'El código de relleno debe ser alfanumérico';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuración FTP
|
||||||
|
if (formData.ftp_server?.trim()) {
|
||||||
|
const hostRegex =
|
||||||
|
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
||||||
|
const ipRegex =
|
||||||
|
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
if (!hostRegex.test(formData.ftp_server) && !ipRegex.test(formData.ftp_server)) {
|
||||||
|
errors.ftp_server = 'Servidor FTP inválido (debe ser un host o IP)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (formData.ftp_user?.trim() && /\s/.test(formData.ftp_user)) {
|
||||||
|
errors.ftp_user = 'El usuario FTP no puede contener espacios';
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldErrors = errors;
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFieldError(field: string) {
|
||||||
|
if (fieldErrors[field]) {
|
||||||
|
fieldErrors[field] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
error = null;
|
error = null;
|
||||||
|
fieldErrors = {};
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
error = 'Por favor, corrige los errores en el formulario';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -131,11 +213,6 @@
|
|||||||
throw new Error('No hay una compañía seleccionada');
|
throw new Error('No hay una compañía seleccionada');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validación básica
|
|
||||||
if (!formData.transporter_key.trim()) {
|
|
||||||
throw new Error('La clave es requerida');
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (isEdit && item) {
|
if (isEdit && item) {
|
||||||
response = await transportersApi.update(item.transporter_key, formData, companyId);
|
response = await transportersApi.update(item.transporter_key, formData, companyId);
|
||||||
@@ -144,9 +221,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
const ve = (response as { validationErrors?: { msg?: string }[] }).validationErrors;
|
const ve = response.validationErrors;
|
||||||
if (ve?.length) {
|
if (ve?.length) {
|
||||||
throw new Error(ve.map((e) => e.msg).join(' · '));
|
// Mapear errores de validación del backend si están disponibles
|
||||||
|
const backendErrors: Record<string, string> = {};
|
||||||
|
ve.forEach((err) => {
|
||||||
|
if (err.field) {
|
||||||
|
backendErrors[err.field] = err.message || 'Error de validación';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fieldErrors = backendErrors;
|
||||||
|
throw new Error('Errores de validación en el servidor');
|
||||||
}
|
}
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
@@ -180,7 +265,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
<Dialog.Root bind:open>
|
||||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl">
|
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl" onInteractOutside={(e) => e.preventDefault()}>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>{title}</Dialog.Title>
|
<Dialog.Title>{title}</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
@@ -214,14 +299,28 @@
|
|||||||
id="transporter_key"
|
id="transporter_key"
|
||||||
bind:value={formData.transporter_key}
|
bind:value={formData.transporter_key}
|
||||||
disabled={isEdit}
|
disabled={isEdit}
|
||||||
|
aria-invalid={!!fieldErrors.transporter_key}
|
||||||
|
oninput={() => clearFieldError('transporter_key')}
|
||||||
required
|
required
|
||||||
maxlength={30}
|
maxlength={30}
|
||||||
/>
|
/>
|
||||||
|
{#if fieldErrors.transporter_key}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.transporter_key}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="name">Nombre / Razón Social</Label>
|
<Label for="name">Nombre / Razón Social <span class="text-destructive">*</span></Label>
|
||||||
<Input id="name" bind:value={formData.name} maxlength={256} />
|
<Input
|
||||||
|
id="name"
|
||||||
|
bind:value={formData.name}
|
||||||
|
aria-invalid={!!fieldErrors.name}
|
||||||
|
oninput={() => clearFieldError('name')}
|
||||||
|
maxlength={256}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.name}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.name}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -229,19 +328,42 @@
|
|||||||
<Input
|
<Input
|
||||||
id="short_name"
|
id="short_name"
|
||||||
bind:value={formData.short_name}
|
bind:value={formData.short_name}
|
||||||
|
aria-invalid={!!fieldErrors.short_name}
|
||||||
|
oninput={() => clearFieldError('short_name')}
|
||||||
maxlength={10}
|
maxlength={10}
|
||||||
placeholder="Máx. 10 car."
|
placeholder="Máx. 10 car."
|
||||||
/>
|
/>
|
||||||
|
{#if fieldErrors.short_name}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.short_name}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="rfc">RFC</Label>
|
<Label for="rfc">RFC</Label>
|
||||||
<Input id="rfc" bind:value={formData.rfc} maxlength={30} />
|
<Input
|
||||||
|
id="rfc"
|
||||||
|
bind:value={formData.rfc}
|
||||||
|
aria-invalid={!!fieldErrors.rfc}
|
||||||
|
oninput={() => clearFieldError('rfc')}
|
||||||
|
maxlength={30}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.rfc}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.rfc}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="responsible">Responsable</Label>
|
<Label for="responsible">Responsable</Label>
|
||||||
<Input id="responsible" bind:value={formData.responsible} maxlength={100} />
|
<Input
|
||||||
|
id="responsible"
|
||||||
|
bind:value={formData.responsible}
|
||||||
|
aria-invalid={!!fieldErrors.responsible}
|
||||||
|
oninput={() => clearFieldError('responsible')}
|
||||||
|
maxlength={100}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.responsible}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.responsible}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -251,7 +373,16 @@
|
|||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="caat_code">Código CAAT</Label>
|
<Label for="caat_code">Código CAAT</Label>
|
||||||
<Input id="caat_code" bind:value={formData.caat_code} maxlength={49} />
|
<Input
|
||||||
|
id="caat_code"
|
||||||
|
bind:value={formData.caat_code}
|
||||||
|
aria-invalid={!!fieldErrors.caat_code}
|
||||||
|
oninput={() => clearFieldError('caat_code')}
|
||||||
|
maxlength={49}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.caat_code}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.caat_code}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -259,9 +390,14 @@
|
|||||||
<Input
|
<Input
|
||||||
id="transport_code"
|
id="transport_code"
|
||||||
bind:value={formData.transport_code}
|
bind:value={formData.transport_code}
|
||||||
|
aria-invalid={!!fieldErrors.transport_code}
|
||||||
|
oninput={() => clearFieldError('transport_code')}
|
||||||
maxlength={8}
|
maxlength={8}
|
||||||
placeholder="Máx. 8 car."
|
placeholder="Máx. 8 car."
|
||||||
/>
|
/>
|
||||||
|
{#if fieldErrors.transport_code}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.transport_code}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -269,9 +405,14 @@
|
|||||||
<Input
|
<Input
|
||||||
id="loader_code"
|
id="loader_code"
|
||||||
bind:value={formData.loader_code}
|
bind:value={formData.loader_code}
|
||||||
|
aria-invalid={!!fieldErrors.loader_code}
|
||||||
|
oninput={() => clearFieldError('loader_code')}
|
||||||
maxlength={9}
|
maxlength={9}
|
||||||
placeholder="Máx. 9 car."
|
placeholder="Máx. 9 car."
|
||||||
/>
|
/>
|
||||||
|
{#if fieldErrors.loader_code}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.loader_code}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -279,13 +420,27 @@
|
|||||||
<Input
|
<Input
|
||||||
id="transport_interface_type"
|
id="transport_interface_type"
|
||||||
bind:value={formData.transport_interface_type}
|
bind:value={formData.transport_interface_type}
|
||||||
|
aria-invalid={!!fieldErrors.transport_interface_type}
|
||||||
|
oninput={() => clearFieldError('transport_interface_type')}
|
||||||
maxlength={20}
|
maxlength={20}
|
||||||
/>
|
/>
|
||||||
|
{#if fieldErrors.transport_interface_type}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.transport_interface_type}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="filler_code">Código Relleno</Label>
|
<Label for="filler_code">Código Relleno</Label>
|
||||||
<Input id="filler_code" bind:value={formData.filler_code} maxlength={20} />
|
<Input
|
||||||
|
id="filler_code"
|
||||||
|
bind:value={formData.filler_code}
|
||||||
|
aria-invalid={!!fieldErrors.filler_code}
|
||||||
|
oninput={() => clearFieldError('filler_code')}
|
||||||
|
maxlength={20}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.filler_code}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.filler_code}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-3 rounded-lg border p-4">
|
<div class="flex items-center justify-between gap-3 rounded-lg border p-4">
|
||||||
@@ -300,18 +455,44 @@
|
|||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="streets">Calle y Número</Label>
|
<Label for="streets">Calle y Número</Label>
|
||||||
<Input id="streets" bind:value={formData.streets} maxlength={100} />
|
<Input
|
||||||
|
id="streets"
|
||||||
|
bind:value={formData.streets}
|
||||||
|
aria-invalid={!!fieldErrors.streets}
|
||||||
|
oninput={() => clearFieldError('streets')}
|
||||||
|
maxlength={100}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.streets}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.streets}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="city">Ciudad</Label>
|
<Label for="city">Ciudad</Label>
|
||||||
<Input id="city" bind:value={formData.city} maxlength={30} />
|
<Input
|
||||||
|
id="city"
|
||||||
|
bind:value={formData.city}
|
||||||
|
aria-invalid={!!fieldErrors.city}
|
||||||
|
oninput={() => clearFieldError('city')}
|
||||||
|
maxlength={30}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.city}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.city}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="country">País (clave americana)</Label>
|
<Label for="country">País (clave americana)</Label>
|
||||||
<Select.Root type="single" bind:value={formData.country} disabled={refsLoading}>
|
<Select.Root
|
||||||
<Select.Trigger class="w-full" id="country">
|
type="single"
|
||||||
|
bind:value={formData.country}
|
||||||
|
disabled={refsLoading}
|
||||||
|
onValueChange={() => clearFieldError('country')}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
class={fieldErrors.country ? 'border-destructive' : ''}
|
||||||
|
id="country"
|
||||||
|
>
|
||||||
{refsLoading
|
{refsLoading
|
||||||
? '...'
|
? '...'
|
||||||
: formData.country
|
: formData.country
|
||||||
@@ -327,14 +508,25 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
|
{#if fieldErrors.country}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.country}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="state">Estado / provincia</Label>
|
<Label for="state">Estado / provincia</Label>
|
||||||
<Select.Root type="single" bind:value={formData.state} disabled={refsLoading}>
|
<Select.Root
|
||||||
<Select.Trigger class="w-full" id="state">
|
type="single"
|
||||||
|
bind:value={formData.state}
|
||||||
|
disabled={refsLoading}
|
||||||
|
onValueChange={() => clearFieldError('state')}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
class={fieldErrors.state ? 'border-destructive' : ''}
|
||||||
|
id="state"
|
||||||
|
>
|
||||||
{refsLoading
|
{refsLoading
|
||||||
? '...'
|
? '...'
|
||||||
: formData.state ||
|
: formData.state ||
|
||||||
@@ -349,10 +541,22 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
|
{#if fieldErrors.state}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.state}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="postal_code">C.P.</Label>
|
<Label for="postal_code">C.P.</Label>
|
||||||
<Input id="postal_code" bind:value={formData.postal_code} maxlength={15} />
|
<Input
|
||||||
|
id="postal_code"
|
||||||
|
bind:value={formData.postal_code}
|
||||||
|
aria-invalid={!!fieldErrors.postal_code}
|
||||||
|
oninput={() => clearFieldError('postal_code')}
|
||||||
|
maxlength={15}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.postal_code}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.postal_code}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -363,23 +567,60 @@
|
|||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ftp_server">Servidor FTP</Label>
|
<Label for="ftp_server">Servidor FTP</Label>
|
||||||
<Input id="ftp_server" bind:value={formData.ftp_server} maxlength={200} />
|
<Input
|
||||||
|
id="ftp_server"
|
||||||
|
bind:value={formData.ftp_server}
|
||||||
|
aria-invalid={!!fieldErrors.ftp_server}
|
||||||
|
oninput={() => clearFieldError('ftp_server')}
|
||||||
|
maxlength={200}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.ftp_server}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.ftp_server}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ftp_user">Usuario</Label>
|
<Label for="ftp_user">Usuario</Label>
|
||||||
<Input id="ftp_user" bind:value={formData.ftp_user} maxlength={200} />
|
<Input
|
||||||
|
id="ftp_user"
|
||||||
|
bind:value={formData.ftp_user}
|
||||||
|
aria-invalid={!!fieldErrors.ftp_user}
|
||||||
|
oninput={() => clearFieldError('ftp_user')}
|
||||||
|
maxlength={200}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.ftp_user}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.ftp_user}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ftp_password">Contraseña</Label>
|
<Label for="ftp_password">Contraseña</Label>
|
||||||
<Input id="ftp_password" type="password" bind:value={formData.ftp_password} maxlength={100} />
|
<Input
|
||||||
|
id="ftp_password"
|
||||||
|
type="password"
|
||||||
|
bind:value={formData.ftp_password}
|
||||||
|
aria-invalid={!!fieldErrors.ftp_password}
|
||||||
|
oninput={() => clearFieldError('ftp_password')}
|
||||||
|
maxlength={100}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.ftp_password}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.ftp_password}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ftp_directory">Directorio</Label>
|
<Label for="ftp_directory">Directorio</Label>
|
||||||
<Input id="ftp_directory" bind:value={formData.ftp_directory} maxlength={1000} />
|
<Input
|
||||||
|
id="ftp_directory"
|
||||||
|
bind:value={formData.ftp_directory}
|
||||||
|
aria-invalid={!!fieldErrors.ftp_directory}
|
||||||
|
oninput={() => clearFieldError('ftp_directory')}
|
||||||
|
maxlength={1000}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.ftp_directory}
|
||||||
|
<p class="text-xs text-destructive">{fieldErrors.ftp_directory}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
<Dialog.Root bind:open>
|
||||||
<Dialog.Content class="max-h-[95vh] max-w-3xl overflow-y-auto">
|
<Dialog.Content class="max-h-[95vh] max-w-3xl overflow-y-auto" onInteractOutside={(e) => e.preventDefault()}>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>{title}</Dialog.Title>
|
<Dialog.Title>{title}</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
|
|||||||
Reference in New Issue
Block a user