- Updated the "Nueva Sección" button in customs_sections, customs_warehouses, incoterms, invoice_types, material_types, payment_methods, pedimento_codes, pedimento_regimens, sectors, states, transport_modes, and transport_types pages to use the Plus icon from Lucide. - Updated the "Actualizar" button in the same pages to use the RefreshCw icon from Lucide. - Added a new edit dialog component for customs brokers with a comprehensive form for editing broker details, including validation and loading states.
444 lines
12 KiB
Svelte
444 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { Button } from "$lib/components/ui/button";
|
|
import * as Dialog from "$lib/components/ui/dialog";
|
|
import { Input } from "$lib/components/ui/input";
|
|
import { Label } from "$lib/components/ui/label";
|
|
import { clientsProvidersApi, type ClientProvider, type CreateClientProviderData, type UpdateClientProviderData } from "$lib/api/dashboard/a76/clients-providers";
|
|
import { companyStore } from "$lib/stores/company.svelte";
|
|
import { LoaderCircle } from 'lucide-svelte';
|
|
|
|
let {
|
|
open = $bindable(false),
|
|
item = $bindable<ClientProvider | null>(null),
|
|
onSuccess
|
|
}: {
|
|
open: boolean;
|
|
item?: ClientProvider | null;
|
|
onSuccess?: () => void;
|
|
} = $props();
|
|
|
|
let formData = $state({
|
|
rfc: "",
|
|
name: "",
|
|
curp: "",
|
|
residence_country: "",
|
|
domicile_fiscal: "",
|
|
foreign_tax_id: "",
|
|
client_or_provider: "client",
|
|
is_active: true,
|
|
// Address fields
|
|
street: "",
|
|
neighborhood: "",
|
|
city: "",
|
|
state: "",
|
|
country: "",
|
|
zip_code: "",
|
|
// Programs fields
|
|
program_code: "",
|
|
authorization_date: ""
|
|
});
|
|
|
|
let loading = $state(false);
|
|
let error = $state<string | null>(null);
|
|
|
|
// Actualizar formData cuando item cambia
|
|
$effect(() => {
|
|
if (item) {
|
|
formData = {
|
|
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 || "",
|
|
client_or_provider: item.client_or_provider || "client",
|
|
is_active: item.is_active ?? true,
|
|
street: item.address?.street || "",
|
|
neighborhood: item.address?.neighborhood || "",
|
|
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 || ""
|
|
};
|
|
} else {
|
|
formData = {
|
|
rfc: "",
|
|
name: "",
|
|
curp: "",
|
|
residence_country: "",
|
|
domicile_fiscal: "",
|
|
foreign_tax_id: "",
|
|
client_or_provider: "client",
|
|
is_active: true,
|
|
street: "",
|
|
neighborhood: "",
|
|
city: "",
|
|
state: "",
|
|
country: "",
|
|
zip_code: "",
|
|
program_code: "",
|
|
authorization_date: ""
|
|
};
|
|
}
|
|
});
|
|
|
|
const isEditing = $derived(!!item);
|
|
|
|
async function handleSubmit(e: Event) {
|
|
e.preventDefault();
|
|
|
|
if (!companyStore.activeCompany) {
|
|
error = "No hay compañía seleccionada";
|
|
return;
|
|
}
|
|
|
|
loading = true;
|
|
error = null;
|
|
|
|
try {
|
|
let response;
|
|
if (isEditing && item) {
|
|
const payload: UpdateClientProviderData = {
|
|
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,
|
|
is_active: formData.is_active,
|
|
address: {
|
|
street: formData.street || null,
|
|
neighborhood: formData.neighborhood || null,
|
|
city: formData.city || null,
|
|
state: formData.state || null,
|
|
country: formData.country || null,
|
|
zip_code: formData.zip_code || null,
|
|
},
|
|
programs: {
|
|
program_code: formData.program_code || null,
|
|
authorization_date: formData.authorization_date || null,
|
|
|
|
}
|
|
};
|
|
response = await clientsProvidersApi.update(item.id, companyStore.activeCompany.id, payload);
|
|
} else {
|
|
const payload: CreateClientProviderData = {
|
|
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,
|
|
is_active: formData.is_active,
|
|
address: {
|
|
street: formData.street || null,
|
|
neighborhood: formData.neighborhood || null,
|
|
city: formData.city || null,
|
|
state: formData.state || null,
|
|
country: formData.country || null,
|
|
zip_code: formData.zip_code || null,
|
|
},
|
|
programs: {
|
|
program_code: formData.program_code || null,
|
|
authorization_date: formData.authorization_date || null,
|
|
}
|
|
};
|
|
response = await clientsProvidersApi.create(companyStore.activeCompany.id, payload);
|
|
}
|
|
|
|
if (response.error) {
|
|
if (response.status === 401) {
|
|
error = 'Sesión expirada. Recargando página...';
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
} else {
|
|
error = response.error;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Éxito
|
|
open = false;
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
}
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : "Error al guardar";
|
|
console.error("Error saving:", e);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function handleOpenChange(newOpen: boolean) {
|
|
if (!newOpen) {
|
|
// Limpiar form al cerrar
|
|
formData = {
|
|
rfc: "",
|
|
name: "",
|
|
curp: "",
|
|
residence_country: "",
|
|
domicile_fiscal: "",
|
|
foreign_tax_id: "",
|
|
client_or_provider: "client",
|
|
is_active: true,
|
|
street: "",
|
|
neighborhood: "",
|
|
city: "",
|
|
state: "",
|
|
country: "",
|
|
zip_code: "",
|
|
program_code: "",
|
|
authorization_date: ""
|
|
};
|
|
error = null;
|
|
}
|
|
open = newOpen;
|
|
}
|
|
</script>
|
|
|
|
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
|
|
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
|
<Dialog.Header>
|
|
<Dialog.Title>
|
|
{isEditing ? "Editar" : "Nuevo"} Cliente/Proveedor
|
|
</Dialog.Title>
|
|
<Dialog.Description>
|
|
{isEditing
|
|
? "Modifica los datos del cliente o proveedor."
|
|
: "Completa los datos para crear un nuevo cliente o proveedor."}
|
|
</Dialog.Description>
|
|
</Dialog.Header>
|
|
|
|
<form onsubmit={handleSubmit} class="space-y-6">
|
|
{#if error}
|
|
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
|
{error}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Información básica -->
|
|
<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="space-y-2">
|
|
<Label for="rfc">RFC *</Label>
|
|
<Input
|
|
id="rfc"
|
|
bind:value={formData.rfc}
|
|
placeholder="Ej: XAXX010101000"
|
|
maxlength={13}
|
|
required
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="client_or_provider">Tipo *</Label>
|
|
<select
|
|
id="client_or_provider"
|
|
bind:value={formData.client_or_provider}
|
|
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="client">Cliente</option>
|
|
<option value="provider">Proveedor</option>
|
|
<option value="both">Ambos</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="name">Nombre / Razón Social *</Label>
|
|
<Input
|
|
id="name"
|
|
bind:value={formData.name}
|
|
placeholder="Nombre completo o razón social"
|
|
maxlength={256}
|
|
required
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="curp">CURP</Label>
|
|
<Input
|
|
id="curp"
|
|
bind:value={formData.curp}
|
|
placeholder="Ej: XAXX010101HDFXXX00"
|
|
maxlength={18}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="residence_country">País de Residencia</Label>
|
|
<Input
|
|
id="residence_country"
|
|
bind:value={formData.residence_country}
|
|
placeholder="Ej: México"
|
|
maxlength={64}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</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>
|
|
<Input
|
|
id="street"
|
|
bind:value={formData.street}
|
|
placeholder="Calle y número"
|
|
maxlength={256}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="neighborhood">Colonia</Label>
|
|
<Input
|
|
id="neighborhood"
|
|
bind:value={formData.neighborhood}
|
|
placeholder="Colonia o barrio"
|
|
maxlength={128}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="zip_code">Código Postal</Label>
|
|
<Input
|
|
id="zip_code"
|
|
bind:value={formData.zip_code}
|
|
placeholder="Ej: 12345"
|
|
maxlength={10}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="city">Ciudad</Label>
|
|
<Input
|
|
id="city"
|
|
bind:value={formData.city}
|
|
placeholder="Ciudad"
|
|
maxlength={128}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="state">Estado</Label>
|
|
<Input
|
|
id="state"
|
|
bind:value={formData.state}
|
|
placeholder="Estado o provincia"
|
|
maxlength={128}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="country">País</Label>
|
|
<Input
|
|
id="country"
|
|
bind:value={formData.country}
|
|
placeholder="País"
|
|
maxlength={64}
|
|
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="space-y-2">
|
|
<Label for="program_code">Código de Programa</Label>
|
|
<Input
|
|
id="program_code"
|
|
bind:value={formData.program_code}
|
|
placeholder="Código del programa"
|
|
maxlength={32}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="authorization_date">Fecha de Autorización</Label>
|
|
<Input
|
|
id="authorization_date"
|
|
type="date"
|
|
bind:value={formData.authorization_date}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Dialog.Footer>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onclick={() => (open = false)}
|
|
disabled={loading}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button type="submit" disabled={loading}>
|
|
{#if loading}
|
|
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
|
{/if}
|
|
{isEditing ? "Guardar cambios" : "Crear"}
|
|
</Button>
|
|
</Dialog.Footer>
|
|
</form>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|