|
|
|
|
@@ -52,6 +52,7 @@
|
|
|
|
|
|
|
|
|
|
let loading = $state(false);
|
|
|
|
|
let error = $state<string | null>(null);
|
|
|
|
|
let fieldErrors = $state<Record<string, string>>({});
|
|
|
|
|
let countries = $state<Country[]>([]);
|
|
|
|
|
let states = $state<State[]>([]);
|
|
|
|
|
let refsLoading = $state(false);
|
|
|
|
|
@@ -109,6 +110,7 @@
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
error = null;
|
|
|
|
|
fieldErrors = {};
|
|
|
|
|
loading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -120,9 +122,89 @@
|
|
|
|
|
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() {
|
|
|
|
|
if (loading) return;
|
|
|
|
|
error = null;
|
|
|
|
|
fieldErrors = {};
|
|
|
|
|
|
|
|
|
|
if (!validateForm()) {
|
|
|
|
|
error = 'Por favor, corrige los errores en el formulario';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
@@ -131,11 +213,6 @@
|
|
|
|
|
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;
|
|
|
|
|
if (isEdit && item) {
|
|
|
|
|
response = await transportersApi.update(item.transporter_key, formData, companyId);
|
|
|
|
|
@@ -144,9 +221,17 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.error) {
|
|
|
|
|
const ve = (response as { validationErrors?: { msg?: string }[] }).validationErrors;
|
|
|
|
|
const ve = response.validationErrors;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
@@ -180,7 +265,7 @@
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<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.Title>{title}</Dialog.Title>
|
|
|
|
|
<Dialog.Description>
|
|
|
|
|
@@ -214,14 +299,28 @@
|
|
|
|
|
id="transporter_key"
|
|
|
|
|
bind:value={formData.transporter_key}
|
|
|
|
|
disabled={isEdit}
|
|
|
|
|
aria-invalid={!!fieldErrors.transporter_key}
|
|
|
|
|
oninput={() => clearFieldError('transporter_key')}
|
|
|
|
|
required
|
|
|
|
|
maxlength={30}
|
|
|
|
|
/>
|
|
|
|
|
{#if fieldErrors.transporter_key}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.transporter_key}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<Label for="name">Nombre / Razón Social</Label>
|
|
|
|
|
<Input id="name" bind:value={formData.name} maxlength={256} />
|
|
|
|
|
<Label for="name">Nombre / Razón Social <span class="text-destructive">*</span></Label>
|
|
|
|
|
<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 class="grid gap-2">
|
|
|
|
|
@@ -229,19 +328,42 @@
|
|
|
|
|
<Input
|
|
|
|
|
id="short_name"
|
|
|
|
|
bind:value={formData.short_name}
|
|
|
|
|
aria-invalid={!!fieldErrors.short_name}
|
|
|
|
|
oninput={() => clearFieldError('short_name')}
|
|
|
|
|
maxlength={10}
|
|
|
|
|
placeholder="Máx. 10 car."
|
|
|
|
|
/>
|
|
|
|
|
{#if fieldErrors.short_name}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.short_name}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="grid gap-2">
|
|
|
|
|
<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>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
@@ -251,7 +373,16 @@
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="grid gap-2">
|
|
|
|
|
@@ -259,9 +390,14 @@
|
|
|
|
|
<Input
|
|
|
|
|
id="transport_code"
|
|
|
|
|
bind:value={formData.transport_code}
|
|
|
|
|
aria-invalid={!!fieldErrors.transport_code}
|
|
|
|
|
oninput={() => clearFieldError('transport_code')}
|
|
|
|
|
maxlength={8}
|
|
|
|
|
placeholder="Máx. 8 car."
|
|
|
|
|
/>
|
|
|
|
|
{#if fieldErrors.transport_code}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.transport_code}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
@@ -269,9 +405,14 @@
|
|
|
|
|
<Input
|
|
|
|
|
id="loader_code"
|
|
|
|
|
bind:value={formData.loader_code}
|
|
|
|
|
aria-invalid={!!fieldErrors.loader_code}
|
|
|
|
|
oninput={() => clearFieldError('loader_code')}
|
|
|
|
|
maxlength={9}
|
|
|
|
|
placeholder="Máx. 9 car."
|
|
|
|
|
/>
|
|
|
|
|
{#if fieldErrors.loader_code}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.loader_code}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
@@ -279,13 +420,27 @@
|
|
|
|
|
<Input
|
|
|
|
|
id="transport_interface_type"
|
|
|
|
|
bind:value={formData.transport_interface_type}
|
|
|
|
|
aria-invalid={!!fieldErrors.transport_interface_type}
|
|
|
|
|
oninput={() => clearFieldError('transport_interface_type')}
|
|
|
|
|
maxlength={20}
|
|
|
|
|
/>
|
|
|
|
|
{#if fieldErrors.transport_interface_type}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.transport_interface_type}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="flex items-center justify-between gap-3 rounded-lg border p-4">
|
|
|
|
|
@@ -300,18 +455,44 @@
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="grid grid-cols-2 gap-4">
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="grid gap-2">
|
|
|
|
|
<Label for="country">País (clave americana)</Label>
|
|
|
|
|
<Select.Root type="single" bind:value={formData.country} disabled={refsLoading}>
|
|
|
|
|
<Select.Trigger class="w-full" id="country">
|
|
|
|
|
<Select.Root
|
|
|
|
|
type="single"
|
|
|
|
|
bind:value={formData.country}
|
|
|
|
|
disabled={refsLoading}
|
|
|
|
|
onValueChange={() => clearFieldError('country')}
|
|
|
|
|
>
|
|
|
|
|
<Select.Trigger
|
|
|
|
|
class={fieldErrors.country ? 'border-destructive' : ''}
|
|
|
|
|
id="country"
|
|
|
|
|
>
|
|
|
|
|
{refsLoading
|
|
|
|
|
? '...'
|
|
|
|
|
: formData.country
|
|
|
|
|
@@ -327,14 +508,25 @@
|
|
|
|
|
{/each}
|
|
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
|
|
|
|
{#if fieldErrors.country}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.country}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<Label for="state">Estado / provincia</Label>
|
|
|
|
|
<Select.Root type="single" bind:value={formData.state} disabled={refsLoading}>
|
|
|
|
|
<Select.Trigger class="w-full" id="state">
|
|
|
|
|
<Select.Root
|
|
|
|
|
type="single"
|
|
|
|
|
bind:value={formData.state}
|
|
|
|
|
disabled={refsLoading}
|
|
|
|
|
onValueChange={() => clearFieldError('state')}
|
|
|
|
|
>
|
|
|
|
|
<Select.Trigger
|
|
|
|
|
class={fieldErrors.state ? 'border-destructive' : ''}
|
|
|
|
|
id="state"
|
|
|
|
|
>
|
|
|
|
|
{refsLoading
|
|
|
|
|
? '...'
|
|
|
|
|
: formData.state ||
|
|
|
|
|
@@ -349,10 +541,22 @@
|
|
|
|
|
{/each}
|
|
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
|
|
|
|
{#if fieldErrors.state}
|
|
|
|
|
<p class="text-xs text-destructive">{fieldErrors.state}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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>
|
|
|
|
|
</section>
|
|
|
|
|
@@ -363,23 +567,60 @@
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="grid grid-cols-2 gap-4">
|
|
|
|
|
<div class="grid gap-2">
|
|
|
|
|
<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 class="grid gap-2">
|
|
|
|
|
<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 class="grid gap-2">
|
|
|
|
|
<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>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
|