Merge pull request 'feature/client-provider-taxid-rfc-fix' (#218) from feature/client-provider-taxid-rfc-fix into development

Reviewed-on: ADUANASOFT/anexo76#218
This commit is contained in:
2026-03-18 03:51:43 +00:00
24 changed files with 519 additions and 233 deletions

View File

@@ -17,7 +17,7 @@ export function createColumns(onSuccess) {
},
{
accessorKey: "rfc",
header: "RFC",
header: "RFC / TAX-ID",
cell: ({ row }) => {
const snippet = createRawSnippet((getData) => {
const { val } = getData();

View File

@@ -21,21 +21,26 @@
rfc: "",
name: "",
curp: "",
residence_country: "",
domicile_fiscal: "",
foreign_tax_id: "",
type_nat_foreign: "N",
client_or_provider: "client",
is_active: true,
// Address fields
street: "",
streets: "",
exterior_number: "",
interior_number: "",
neighborhood: "",
municipality: "",
city: "",
state: "",
country: "",
zip_code: "",
// Programs fields
program_code: "",
authorization_date: ""
postal_code: "",
email: "",
phone: "",
contact: "",
// Programs fields (subset)
program: "",
program_number: "",
manufacturer_id: ""
});
let loading = $state(false);
@@ -48,43 +53,59 @@
rfc: item.rfc,
name: item.name,
curp: item.curp || "",
residence_country: item.residence_country || "",
domicile_fiscal: item.domicile_fiscal || "",
foreign_tax_id: item.foreign_tax_id || "",
type_nat_foreign: item.type_nat_foreign || "N",
client_or_provider: item.client_or_provider || "client",
is_active: item.is_active ?? true,
street: item.address?.street || "",
streets: item.address?.streets || "",
exterior_number: item.address?.exterior_number || "",
interior_number: item.address?.interior_number || "",
neighborhood: item.address?.neighborhood || "",
municipality: item.address?.municipality || "",
city: item.address?.city || "",
state: item.address?.state || "",
country: item.address?.country || "",
zip_code: item.address?.zip_code || "",
program_code: item.programs?.program_code || "",
authorization_date: item.programs?.authorization_date || ""
postal_code: item.address?.postal_code || "",
email: item.address?.email || "",
phone: item.address?.phone || "",
contact: item.address?.contact || "",
program: item.programs?.program || "",
program_number: item.programs?.program_number || "",
manufacturer_id: item.programs?.manufacturer_id || ""
};
} else {
formData = {
rfc: "",
name: "",
curp: "",
residence_country: "",
domicile_fiscal: "",
foreign_tax_id: "",
type_nat_foreign: "N",
client_or_provider: "client",
is_active: true,
street: "",
streets: "",
exterior_number: "",
interior_number: "",
neighborhood: "",
municipality: "",
city: "",
state: "",
country: "",
zip_code: "",
program_code: "",
authorization_date: ""
postal_code: "",
email: "",
phone: "",
contact: "",
program: "",
program_number: "",
manufacturer_id: ""
};
}
});
const isEditing = $derived(!!item);
const isForeign = $derived((formData.type_nat_foreign || "N").toUpperCase() === "E");
// Validación de formato: RFC (Nacional) o TAX-ID (Extranjero)
const RFC_REGEX = /^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i;
// TAX-ID: 2 dígitos, guión, resto alfanumérico. Ej: 12-3456789. Máx 30 caracteres.
const TAX_ID_REGEX = /^\d{2}-[A-Z0-9]{1,27}$/i;
async function handleSubmit(e: Event) {
e.preventDefault();
@@ -94,6 +115,19 @@
return;
}
const rfcVal = (formData.rfc || "").trim();
if (isForeign) {
if (!TAX_ID_REGEX.test(rfcVal)) {
error = "El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres.";
return;
}
} else {
if (!RFC_REGEX.test(rfcVal)) {
error = "El RFC no tiene el formato correcto. Ejemplo: XAXX010101000.";
return;
}
}
loading = true;
error = null;
@@ -104,23 +138,27 @@
rfc: formData.rfc,
name: formData.name,
curp: formData.curp || null,
residence_country: formData.residence_country || null,
domicile_fiscal: formData.domicile_fiscal || null,
foreign_tax_id: formData.foreign_tax_id || null,
client_or_provider: formData.client_or_provider as "client" | "provider" | "both" | null,
type_nat_foreign: (formData.type_nat_foreign || null) as any,
is_active: formData.is_active,
address: {
street: formData.street || null,
streets: formData.streets || null,
exterior_number: formData.exterior_number || null,
interior_number: formData.interior_number || null,
neighborhood: formData.neighborhood || null,
municipality: formData.municipality || null,
city: formData.city || null,
state: formData.state || null,
country: formData.country || null,
zip_code: formData.zip_code || null,
postal_code: formData.postal_code || null,
email: formData.email || null,
phone: formData.phone || null,
contact: formData.contact || null,
},
programs: {
program_code: formData.program_code || null,
authorization_date: formData.authorization_date || null,
program: formData.program || null,
program_number: formData.program_number || null,
manufacturer_id: formData.manufacturer_id || null,
}
};
response = await clientsProvidersApi.update(item.id, companyStore.activeCompany.id, payload);
@@ -129,22 +167,27 @@
rfc: formData.rfc,
name: formData.name,
curp: formData.curp || null,
residence_country: formData.residence_country || null,
domicile_fiscal: formData.domicile_fiscal || null,
foreign_tax_id: formData.foreign_tax_id || null,
client_or_provider: formData.client_or_provider as "client" | "provider" | "both" | null,
type_nat_foreign: (formData.type_nat_foreign || null) as any,
is_active: formData.is_active,
address: {
street: formData.street || null,
streets: formData.streets || null,
exterior_number: formData.exterior_number || null,
interior_number: formData.interior_number || null,
neighborhood: formData.neighborhood || null,
municipality: formData.municipality || null,
city: formData.city || null,
state: formData.state || null,
country: formData.country || null,
zip_code: formData.zip_code || null,
postal_code: formData.postal_code || null,
email: formData.email || null,
phone: formData.phone || null,
contact: formData.contact || null,
},
programs: {
program_code: formData.program_code || null,
authorization_date: formData.authorization_date || null,
program: formData.program || null,
program_number: formData.program_number || null,
manufacturer_id: formData.manufacturer_id || null,
}
};
response = await clientsProvidersApi.create(companyStore.activeCompany.id, payload);
@@ -182,19 +225,24 @@
rfc: "",
name: "",
curp: "",
residence_country: "",
domicile_fiscal: "",
foreign_tax_id: "",
type_nat_foreign: "N",
client_or_provider: "client",
is_active: true,
street: "",
streets: "",
exterior_number: "",
interior_number: "",
neighborhood: "",
municipality: "",
city: "",
state: "",
country: "",
zip_code: "",
program_code: "",
authorization_date: ""
postal_code: "",
email: "",
phone: "",
contact: "",
program: "",
program_number: "",
manufacturer_id: ""
};
error = null;
}
@@ -226,14 +274,27 @@
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Básica</h3>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="rfc">RFC *</Label>
<Label for="type_nat_foreign">Procedencia *</Label>
<select
id="type_nat_foreign"
bind:value={formData.type_nat_foreign}
required
disabled={loading}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="N">Nacional</option>
<option value="E">Extranjero</option>
</select>
</div>
<div class="space-y-2">
<Label for="rfc">{isForeign ? "TAX-ID" : "RFC"} *</Label>
<Input
id="rfc"
bind:value={formData.rfc}
placeholder="Ej: XAXX010101000"
maxlength={13}
placeholder={isForeign ? "Ej: 12-3456789 (según país)" : "Ej: XAXX010101000"}
maxlength={30}
required
disabled={loading}
/>
@@ -292,48 +353,45 @@
</div>
</div>
<!-- Información fiscal -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Fiscal</h3>
<div class="space-y-2">
<Label for="domicile_fiscal">Domicilio Fiscal</Label>
<Input
id="domicile_fiscal"
bind:value={formData.domicile_fiscal}
placeholder="Domicilio fiscal completo"
maxlength={256}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="foreign_tax_id">ID Fiscal Extranjero</Label>
<Input
id="foreign_tax_id"
bind:value={formData.foreign_tax_id}
placeholder="Para contribuyentes extranjeros"
maxlength={64}
disabled={loading}
/>
</div>
</div>
<!-- Dirección -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Dirección</h3>
<div class="space-y-2">
<Label for="street">Calle</Label>
<Label for="streets">Calles</Label>
<Input
id="street"
bind:value={formData.street}
placeholder="Calle y número"
id="streets"
bind:value={formData.streets}
placeholder="Calle y referencias"
maxlength={256}
disabled={loading}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="exterior_number">Número exterior</Label>
<Input
id="exterior_number"
bind:value={formData.exterior_number}
placeholder="Ej: 123"
maxlength={20}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="interior_number">Número interior</Label>
<Input
id="interior_number"
bind:value={formData.interior_number}
placeholder="Ej: 4B"
maxlength={20}
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="neighborhood">Colonia</Label>
@@ -347,10 +405,10 @@
</div>
<div class="space-y-2">
<Label for="zip_code">Código Postal</Label>
<Label for="postal_code">Código Postal</Label>
<Input
id="zip_code"
bind:value={formData.zip_code}
id="postal_code"
bind:value={formData.postal_code}
placeholder="Ej: 12345"
maxlength={10}
disabled={loading}
@@ -359,6 +417,17 @@
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="municipality">Municipio</Label>
<Input
id="municipality"
bind:value={formData.municipality}
placeholder="Municipio"
maxlength={150}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="city">Ciudad</Label>
<Input
@@ -392,30 +461,76 @@
disabled={loading}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
bind:value={formData.email}
placeholder="correo@dominio.com"
maxlength={100}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="phone">Teléfono</Label>
<Input
id="phone"
bind:value={formData.phone}
placeholder="Teléfono"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="contact">Contacto</Label>
<Input
id="contact"
bind:value={formData.contact}
placeholder="Persona de contacto"
maxlength={50}
disabled={loading}
/>
</div>
</div>
<!-- Programas -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Programas</h3>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="program_code">Código de Programa</Label>
<Label for="program">Programa</Label>
<Input
id="program_code"
bind:value={formData.program_code}
placeholder="Código del programa"
maxlength={32}
id="program"
bind:value={formData.program}
placeholder="Ej: IMMEX"
maxlength={7}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="authorization_date">Fecha de Autorización</Label>
<Label for="program_number">Número de Programa</Label>
<Input
id="authorization_date"
type="date"
bind:value={formData.authorization_date}
id="program_number"
bind:value={formData.program_number}
placeholder="Número"
maxlength={40}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="manufacturer_id">Manufacturer ID</Label>
<Input
id="manufacturer_id"
bind:value={formData.manufacturer_id}
placeholder="Manufacturer ID"
maxlength={25}
disabled={loading}
/>
</div>

View File

@@ -2,7 +2,7 @@
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { ClientProvider } from "./columns.js";
import type { ClientProvider } from "$lib/api/dashboard/a76/clients-providers";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
import { clientsProvidersApi } from "$lib/api/dashboard/a76/clients-providers";
@@ -29,6 +29,9 @@
navigator.clipboard.writeText(item.rfc);
}
const isForeign = (cp: ClientProvider) =>
(cp.type_nat_foreign || "N").toUpperCase() === "E";
function handleViewDetails() {
showDetailsDialog = true;
}
@@ -77,7 +80,7 @@
Copiar ID
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleCopyRfc}>
Copiar RFC
Copiar {isForeign(item) ? 'TAX-ID' : 'RFC'}
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />

View File

@@ -51,6 +51,9 @@
}
open = newOpen;
}
const isForeign = (cp: ClientProvider) =>
(cp.type_nat_foreign || "N").toUpperCase() === "E";
</script>
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
@@ -66,7 +69,7 @@
<span class="font-semibold">{item.id}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="font-medium">RFC:</span>
<span class="font-medium">{item ? (isForeign(item) ? 'TAX-ID:' : 'RFC:') : 'RFC:'}</span>
<code class="font-mono font-semibold">{item.rfc}</code>
</div>
<div class="flex flex-col gap-1 text-sm">

View File

@@ -15,6 +15,9 @@
function handleOpenChange(newOpen: boolean) {
open = newOpen;
}
const isForeign = (cp: ClientProvider) =>
(cp.type_nat_foreign || "N").toUpperCase() === "E";
</script>
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
@@ -39,7 +42,9 @@
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">RFC</span>
<span class="text-xs font-medium text-muted-foreground">
{isForeign(item) ? "TAX-ID" : "RFC"}
</span>
<code class="text-sm font-mono font-semibold">{item.rfc}</code>
</div>
</div>
@@ -63,7 +68,7 @@
<div class="space-y-2">
<h3 class="text-sm font-semibold">Clasificación</h3>
<div class="grid grid-cols-2 gap-2">
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Tipo</span>
{#if item.client_or_provider === 'client'}
@@ -82,6 +87,13 @@
<span class="text-sm text-muted-foreground">No especificado</span>
{/if}
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Procedencia</span>
<span class="text-sm font-medium">
{isForeign(item) ? "Extranjero" : "Nacional"}
</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Estado</span>
@@ -100,44 +112,16 @@
<Separator />
</div>
<!-- Información fiscal -->
<div class="space-y-2">
<h3 class="text-sm font-semibold">Información Fiscal</h3>
{#if item.residence_country}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">País de Residencia</span>
<span class="text-sm">{item.residence_country}</span>
</div>
{/if}
{#if item.domicile_fiscal}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Domicilio Fiscal</span>
<span class="text-sm">{item.domicile_fiscal}</span>
</div>
{/if}
{#if item.foreign_tax_id}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">ID Fiscal Extranjero</span>
<code class="text-sm font-mono">{item.foreign_tax_id}</code>
</div>
{/if}
<Separator />
</div>
<!-- Dirección (si existe) -->
{#if item.address}
<div class="space-y-2">
<h3 class="text-sm font-semibold">Dirección</h3>
<div class="space-y-1">
{#if item.address.street}
{#if item.address.streets}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Calle</span>
<span class="text-sm">{item.address.street}</span>
<span class="text-xs font-medium text-muted-foreground">Calles</span>
<span class="text-sm">{item.address.streets}</span>
</div>
{/if}
@@ -149,15 +133,22 @@
</div>
{/if}
{#if item.address.zip_code}
{#if item.address.postal_code}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Código Postal</span>
<span class="text-sm">{item.address.zip_code}</span>
<span class="text-sm">{item.address.postal_code}</span>
</div>
{/if}
</div>
<div class="grid grid-cols-2 gap-2">
{#if item.address.municipality}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Municipio</span>
<span class="text-sm">{item.address.municipality}</span>
</div>
{/if}
{#if item.address.city}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Ciudad</span>
@@ -179,6 +170,29 @@
<span class="text-sm">{item.address.country}</span>
</div>
{/if}
{#if item.address.email || item.address.phone || item.address.contact}
<div class="grid grid-cols-2 gap-2 pt-2">
{#if item.address.email}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Email</span>
<span class="text-sm">{item.address.email}</span>
</div>
{/if}
{#if item.address.phone}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Teléfono</span>
<span class="text-sm">{item.address.phone}</span>
</div>
{/if}
{#if item.address.contact}
<div class="flex flex-col gap-1 col-span-2">
<span class="text-xs font-medium text-muted-foreground">Contacto</span>
<span class="text-sm">{item.address.contact}</span>
</div>
{/if}
</div>
{/if}
</div>
<Separator />
@@ -190,17 +204,27 @@
<div class="space-y-2">
<h3 class="text-sm font-semibold">Programas</h3>
{#if item.programs.program_code}
{#if item.programs.program}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Código de Programa</span>
<code class="text-sm font-mono">{item.programs.program_code}</code>
<span class="text-xs font-medium text-muted-foreground">Programa</span>
<code class="text-sm font-mono">{item.programs.program}</code>
</div>
{/if}
{#if item.programs.authorization_date}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Fecha de Autorización</span>
<span class="text-sm">{new Date(item.programs.authorization_date).toLocaleDateString()}</span>
{#if item.programs.program_number || item.programs.manufacturer_id}
<div class="grid grid-cols-2 gap-2">
{#if item.programs.program_number}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Número</span>
<span class="text-sm">{item.programs.program_number}</span>
</div>
{/if}
{#if item.programs.manufacturer_id}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Manufacturer ID</span>
<span class="text-sm font-mono">{item.programs.manufacturer_id}</span>
</div>
{/if}
</div>
{/if}
</div>