refactor(crm): catálogos como páginas completas con pestañas (sin modales)

- Clientes/Prospectos y Proveedores: alta y edición en páginas dedicadas
  (/nuevo y /[id]) respetando el shell del dashboard, en vez de modales
- Info segmentada en pestañas: Generales · Comercial · Fiscal · Observaciones
  · Direcciones · Contactos · Documentos
- Componentes reutilizables AccountFields/SupplierFields; RelatedManager
  ahora renderiza por sección
- svelte-check: 0 errores de tipo en archivos del CRM

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aduanasoft
2026-07-14 17:44:21 -06:00
parent 79135d9b5a
commit c26e04bcff
9 changed files with 551 additions and 375 deletions

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import type { AccountInput } from '$lib/api/crm';
import {
ACCOUNT_TYPES, ACCOUNT_STATUS, RECORD_TYPES, PERSON_TYPES,
COMMERCIAL_CLASSIFICATION, CONTACT_METHODS
} from '$lib/components/crm/format';
// `form` es un objeto reactivo del padre; se mutan sus propiedades vía bind:value.
let { form = $bindable(), tab }: { form: AccountInput; tab: string } = $props();
const inputCls =
'rounded-md border bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring';
</script>
{#if tab === 'generales'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm sm:col-span-2"><span class="font-medium">Razón social *</span><input class={inputCls} bind:value={form.name} required /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Nombre comercial</span><input class={inputCls} bind:value={form.trade_name} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">RFC</span><input class="font-mono {inputCls}" maxlength="13" bind:value={form.rfc} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">CURP</span><input class="font-mono {inputCls}" maxlength="18" bind:value={form.curp} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tipo de registro</span><select class={inputCls} bind:value={form.record_type}>{#each RECORD_TYPES as r (r.value)}<option value={r.value}>{r.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tipo de persona</span><select class={inputCls} bind:value={form.person_type}><option value={undefined}>—</option>{#each PERSON_TYPES as p (p.value)}<option value={p.value}>{p.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Giro / Industria</span><input class={inputCls} bind:value={form.industry} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tipo operativo</span><select class={inputCls} bind:value={form.account_type}><option value={undefined}>—</option>{#each ACCOUNT_TYPES as t (t.value)}<option value={t.value}>{t.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Estatus</span><select class={inputCls} bind:value={form.status}>{#each ACCOUNT_STATUS as s (s.value)}<option value={s.value}>{s.label}</option>{/each}</select></label>
</div>
{:else if tab === 'comercial'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Clasificación</span><select class={inputCls} bind:value={form.commercial_classification}><option value={undefined}>—</option>{#each COMMERCIAL_CLASSIFICATION as c (c.value)}<option value={c.value}>{c.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Medio de contacto preferido</span><select class={inputCls} bind:value={form.preferred_contact_method}><option value={undefined}>—</option>{#each CONTACT_METHODS as m (m.value)}<option value={m.value}>{m.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Idioma</span><input class={inputCls} bind:value={form.language} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Email</span><input type="email" class={inputCls} bind:value={form.email} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Teléfono</span><input class={inputCls} bind:value={form.phone} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Sitio web</span><input class={inputCls} bind:value={form.website} /></label>
</div>
{:else if tab === 'fiscal'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Régimen fiscal</span><input class={inputCls} bind:value={form.tax_regime} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Uso de CFDI</span><input class={inputCls} bind:value={form.cfdi_use} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Método de pago</span><input class={inputCls} bind:value={form.payment_method} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Forma de pago</span><input class={inputCls} bind:value={form.payment_form} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Moneda</span><input class={inputCls} maxlength="3" bind:value={form.currency} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Límite de crédito</span><input type="number" min="0" step="0.01" class={inputCls} bind:value={form.credit_limit} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Días de crédito</span><input type="number" min="0" class={inputCls} bind:value={form.credit_days} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Patente aduanal</span><input class={inputCls} maxlength="20" bind:value={form.patente_aduanal} /></label>
<label class="flex flex-col gap-1 text-sm sm:col-span-2"><span class="font-medium">Condiciones comerciales</span><textarea rows="3" class={inputCls} bind:value={form.commercial_terms}></textarea></label>
</div>
{:else if tab === 'observaciones'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Comentarios</span><textarea rows="4" class={inputCls} bind:value={form.notes}></textarea></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Notas internas</span><textarea rows="4" class={inputCls} bind:value={form.internal_notes}></textarea></label>
</div>
{/if}

View File

@@ -11,8 +11,18 @@
import { ADDRESS_TYPES, DOC_TYPES, CONTACT_AREAS, labelOf } from '$lib/components/crm/format';
import { toast } from 'svelte-sonner';
// Dueño de los registros relacionados: cliente (account) o proveedor (supplier)
let { ownerType, ownerId }: { ownerType: 'account' | 'supplier'; ownerId: number } = $props();
// Dueño de los registros relacionados y qué sección mostrar
let {
ownerType,
ownerId,
section = 'all'
}: {
ownerType: 'account' | 'supplier';
ownerId: number;
section?: 'addresses' | 'contacts' | 'documents' | 'all';
} = $props();
const show = (s: 'addresses' | 'contacts' | 'documents') => section === 'all' || section === s;
let addresses = $state<Address[]>([]);
let contacts = $state<Contact[]>([]);
@@ -120,93 +130,96 @@
</script>
<div class="grid gap-4">
<!-- Direcciones -->
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between">
<Card.Title class="flex items-center gap-2 text-base"><MapPin class="h-4 w-4" /> Direcciones</Card.Title>
<Button size="sm" variant="outline" onclick={() => openModal('address')}><Plus class="mr-1 h-4 w-4" /> Agregar</Button>
</Card.Header>
<Card.Content>
{#if addresses.length === 0}
<p class="text-sm text-muted-foreground">Sin direcciones.</p>
{:else}
<Table.Root>
<Table.Header><Table.Row><Table.Head>Tipo</Table.Head><Table.Head>Domicilio</Table.Head><Table.Head>CP</Table.Head><Table.Head>Ciudad</Table.Head><Table.Head></Table.Head></Table.Row></Table.Header>
<Table.Body>
{#each addresses as a (a.id)}
<Table.Row>
<Table.Cell>{labelOf(ADDRESS_TYPES, a.address_type)}{#if a.is_primary}<span class="ml-1 text-[10px] text-primary">(principal)</span>{/if}</Table.Cell>
<Table.Cell class="text-sm">{[a.street, a.ext_number, a.neighborhood].filter(Boolean).join(' ') || '—'}</Table.Cell>
<Table.Cell>{a.postal_code ?? '—'}</Table.Cell>
<Table.Cell>{[a.city, a.state].filter(Boolean).join(', ') || '—'}</Table.Cell>
<Table.Cell class="text-right"><Button variant="ghost" size="sm" onclick={() => removeAddress(a)} aria-label="Eliminar"><Trash2 class="h-4 w-4 text-destructive" /></Button></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</Card.Content>
</Card.Root>
{#if show('addresses')}
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between">
<Card.Title class="flex items-center gap-2 text-base"><MapPin class="h-4 w-4" /> Direcciones</Card.Title>
<Button size="sm" variant="outline" onclick={() => openModal('address')}><Plus class="mr-1 h-4 w-4" /> Agregar</Button>
</Card.Header>
<Card.Content>
{#if addresses.length === 0}
<p class="text-sm text-muted-foreground">Sin direcciones.</p>
{:else}
<Table.Root>
<Table.Header><Table.Row><Table.Head>Tipo</Table.Head><Table.Head>Domicilio</Table.Head><Table.Head>CP</Table.Head><Table.Head>Ciudad</Table.Head><Table.Head></Table.Head></Table.Row></Table.Header>
<Table.Body>
{#each addresses as a (a.id)}
<Table.Row>
<Table.Cell>{labelOf(ADDRESS_TYPES, a.address_type)}{#if a.is_primary}<span class="ml-1 text-[10px] text-primary">(principal)</span>{/if}</Table.Cell>
<Table.Cell class="text-sm">{[a.street, a.ext_number, a.neighborhood].filter(Boolean).join(' ') || '—'}</Table.Cell>
<Table.Cell>{a.postal_code ?? '—'}</Table.Cell>
<Table.Cell>{[a.city, a.state].filter(Boolean).join(', ') || '—'}</Table.Cell>
<Table.Cell class="text-right"><Button variant="ghost" size="sm" onclick={() => removeAddress(a)} aria-label="Eliminar"><Trash2 class="h-4 w-4 text-destructive" /></Button></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</Card.Content>
</Card.Root>
{/if}
<!-- Contactos -->
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between">
<Card.Title class="flex items-center gap-2 text-base"><Users class="h-4 w-4" /> Contactos</Card.Title>
<Button size="sm" variant="outline" onclick={() => openModal('contact')}><Plus class="mr-1 h-4 w-4" /> Agregar</Button>
</Card.Header>
<Card.Content>
{#if contacts.length === 0}
<p class="text-sm text-muted-foreground">Sin contactos.</p>
{:else}
<Table.Root>
<Table.Header><Table.Row><Table.Head>Nombre</Table.Head><Table.Head>Puesto / Área</Table.Head><Table.Head>Email</Table.Head><Table.Head>Teléfono</Table.Head><Table.Head></Table.Head></Table.Row></Table.Header>
<Table.Body>
{#each contacts as c (c.id)}
<Table.Row>
<Table.Cell class="font-medium">{c.first_name} {c.last_name ?? ''}{#if c.is_primary}<span class="ml-1 text-[10px] text-primary">(principal)</span>{/if}</Table.Cell>
<Table.Cell class="text-sm">{[c.job_title, labelOf(CONTACT_AREAS, c.area) !== '—' ? labelOf(CONTACT_AREAS, c.area) : null].filter(Boolean).join(' · ') || '—'}</Table.Cell>
<Table.Cell>{c.email ?? '—'}</Table.Cell>
<Table.Cell>{c.phone ?? c.mobile ?? '—'}</Table.Cell>
<Table.Cell class="text-right"><Button variant="ghost" size="sm" onclick={() => removeContact(c)} aria-label="Eliminar"><Trash2 class="h-4 w-4 text-destructive" /></Button></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</Card.Content>
</Card.Root>
{#if show('contacts')}
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between">
<Card.Title class="flex items-center gap-2 text-base"><Users class="h-4 w-4" /> Contactos</Card.Title>
<Button size="sm" variant="outline" onclick={() => openModal('contact')}><Plus class="mr-1 h-4 w-4" /> Agregar</Button>
</Card.Header>
<Card.Content>
{#if contacts.length === 0}
<p class="text-sm text-muted-foreground">Sin contactos.</p>
{:else}
<Table.Root>
<Table.Header><Table.Row><Table.Head>Nombre</Table.Head><Table.Head>Puesto / Área</Table.Head><Table.Head>Email</Table.Head><Table.Head>Teléfono</Table.Head><Table.Head></Table.Head></Table.Row></Table.Header>
<Table.Body>
{#each contacts as c (c.id)}
<Table.Row>
<Table.Cell class="font-medium">{c.first_name} {c.last_name ?? ''}{#if c.is_primary}<span class="ml-1 text-[10px] text-primary">(principal)</span>{/if}</Table.Cell>
<Table.Cell class="text-sm">{[c.job_title, labelOf(CONTACT_AREAS, c.area) !== '—' ? labelOf(CONTACT_AREAS, c.area) : null].filter(Boolean).join(' · ') || '—'}</Table.Cell>
<Table.Cell>{c.email ?? '—'}</Table.Cell>
<Table.Cell>{c.phone ?? c.mobile ?? '—'}</Table.Cell>
<Table.Cell class="text-right"><Button variant="ghost" size="sm" onclick={() => removeContact(c)} aria-label="Eliminar"><Trash2 class="h-4 w-4 text-destructive" /></Button></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</Card.Content>
</Card.Root>
{/if}
<!-- Documentos -->
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between">
<Card.Title class="flex items-center gap-2 text-base"><FileText class="h-4 w-4" /> Documentos</Card.Title>
<Button size="sm" variant="outline" onclick={() => openModal('document')}><Plus class="mr-1 h-4 w-4" /> Agregar</Button>
</Card.Header>
<Card.Content>
{#if documents.length === 0}
<p class="text-sm text-muted-foreground">Sin documentos.</p>
{:else}
<Table.Root>
<Table.Header><Table.Row><Table.Head>Tipo</Table.Head><Table.Head>Nombre</Table.Head><Table.Head>Archivo</Table.Head><Table.Head></Table.Head></Table.Row></Table.Header>
<Table.Body>
{#each documents as d (d.id)}
<Table.Row>
<Table.Cell>{labelOf(DOC_TYPES, d.doc_type)}</Table.Cell>
<Table.Cell class="font-medium">{d.name}</Table.Cell>
<Table.Cell>{#if d.file_url}<a class="text-primary hover:underline" href={d.file_url} target="_blank" rel="noopener">Ver</a>{:else}{/if}</Table.Cell>
<Table.Cell class="text-right"><Button variant="ghost" size="sm" onclick={() => removeDocument(d)} aria-label="Eliminar"><Trash2 class="h-4 w-4 text-destructive" /></Button></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</Card.Content>
</Card.Root>
{#if show('documents')}
<Card.Root>
<Card.Header class="flex flex-row items-center justify-between">
<Card.Title class="flex items-center gap-2 text-base"><FileText class="h-4 w-4" /> Documentos</Card.Title>
<Button size="sm" variant="outline" onclick={() => openModal('document')}><Plus class="mr-1 h-4 w-4" /> Agregar</Button>
</Card.Header>
<Card.Content>
{#if documents.length === 0}
<p class="text-sm text-muted-foreground">Sin documentos.</p>
{:else}
<Table.Root>
<Table.Header><Table.Row><Table.Head>Tipo</Table.Head><Table.Head>Nombre</Table.Head><Table.Head>Archivo</Table.Head><Table.Head></Table.Head></Table.Row></Table.Header>
<Table.Body>
{#each documents as d (d.id)}
<Table.Row>
<Table.Cell>{labelOf(DOC_TYPES, d.doc_type)}</Table.Cell>
<Table.Cell class="font-medium">{d.name}</Table.Cell>
<Table.Cell>{#if d.file_url}<a class="text-primary hover:underline" href={d.file_url} target="_blank" rel="noopener">Ver</a>{:else}{/if}</Table.Cell>
<Table.Cell class="text-right"><Button variant="ghost" size="sm" onclick={() => removeDocument(d)} aria-label="Eliminar"><Trash2 class="h-4 w-4 text-destructive" /></Button></Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</Card.Content>
</Card.Root>
{/if}
</div>
{#if activeModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" role="presentation" onclick={() => (activeModal = null)}>
<div class="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-lg border bg-card p-6 shadow-lg" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
<div class="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-lg border bg-card p-6 shadow-lg" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()}>
{#if activeModal === 'address'}
<h3 class="mb-4 text-base font-semibold">Nueva dirección</h3>
<form class="grid gap-3 sm:grid-cols-2" onsubmit={saveAddress}>

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import type { SupplierInput } from '$lib/api/crm';
import { SUPPLIER_CLASSIFICATIONS, COVERAGE, ACCOUNT_STATUS, PERSON_TYPES } from '$lib/components/crm/format';
// listas separadas por coma también son bindables (el padre las convierte a arreglo)
let {
form = $bindable(),
tab,
countriesStr = $bindable(),
portsStr = $bindable(),
airportsStr = $bindable(),
customsStr = $bindable()
}: {
form: SupplierInput;
tab: string;
countriesStr: string;
portsStr: string;
airportsStr: string;
customsStr: string;
} = $props();
const inputCls =
'rounded-md border bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring';
</script>
{#if tab === 'generales'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm sm:col-span-2"><span class="font-medium">Razón social *</span><input class={inputCls} bind:value={form.name} required /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Nombre comercial</span><input class={inputCls} bind:value={form.trade_name} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">RFC</span><input class="font-mono {inputCls}" maxlength="13" bind:value={form.rfc} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tipo de persona</span><select class={inputCls} bind:value={form.person_type}><option value={undefined}>—</option>{#each PERSON_TYPES as p (p.value)}<option value={p.value}>{p.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Estatus</span><select class={inputCls} bind:value={form.status}>{#each ACCOUNT_STATUS as s (s.value)}<option value={s.value}>{s.label}</option>{/each}</select></label>
</div>
<div class="mt-4">
<p class="mb-2 text-sm font-medium">Clasificación (una o varias)</p>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
{#each SUPPLIER_CLASSIFICATIONS as c (c.value)}
<label class="flex items-center gap-2 text-sm"><input type="checkbox" class="h-4 w-4 rounded border" value={c.value} bind:group={form.classifications} /><span>{c.label}</span></label>
{/each}
</div>
</div>
{:else if tab === 'comercial'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Cobertura</span><select class={inputCls} bind:value={form.coverage}><option value={undefined}>—</option>{#each COVERAGE as c (c.value)}<option value={c.value}>{c.label}</option>{/each}</select></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Moneda de cotización</span><input class={inputCls} maxlength="3" bind:value={form.quote_currency} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Países (separados por coma)</span><input class={inputCls} bind:value={countriesStr} placeholder="MX, US, PA" /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Puertos</span><input class={inputCls} bind:value={portsStr} placeholder="Veracruz, Manzanillo" /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Aeropuertos</span><input class={inputCls} bind:value={airportsStr} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Aduanas</span><input class={inputCls} bind:value={customsStr} placeholder="Nuevo Laredo, Colombia" /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Horario de atención</span><input class={inputCls} bind:value={form.business_hours} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tiempo prom. de respuesta</span><input class={inputCls} bind:value={form.avg_response_time} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Email</span><input type="email" class={inputCls} bind:value={form.email} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Teléfono</span><input class={inputCls} bind:value={form.phone} /></label>
<label class="flex flex-col gap-1 text-sm sm:col-span-2"><span class="font-medium">Servicios que ofrece</span><textarea rows="2" class={inputCls} bind:value={form.services_offered}></textarea></label>
<label class="flex flex-col gap-1 text-sm sm:col-span-2"><span class="font-medium">Observaciones comerciales</span><textarea rows="2" class={inputCls} bind:value={form.commercial_notes}></textarea></label>
</div>
{:else if tab === 'fiscal'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Régimen fiscal</span><input class={inputCls} bind:value={form.tax_regime} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Método de pago</span><input class={inputCls} bind:value={form.payment_method} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Forma de pago</span><input class={inputCls} bind:value={form.payment_form} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Límite de crédito</span><input type="number" min="0" step="0.01" class={inputCls} bind:value={form.credit_limit} /></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Días de crédito</span><input type="number" min="0" class={inputCls} bind:value={form.credit_days} /></label>
<label class="flex flex-col gap-1 text-sm sm:col-span-2"><span class="font-medium">Condiciones comerciales</span><textarea rows="3" class={inputCls} bind:value={form.commercial_terms}></textarea></label>
</div>
{:else if tab === 'observaciones'}
<div class="grid gap-4 sm:grid-cols-2">
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Comentarios</span><textarea rows="4" class={inputCls} bind:value={form.notes}></textarea></label>
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Notas internas</span><textarea rows="4" class={inputCls} bind:value={form.internal_notes}></textarea></label>
</div>
{/if}