Merge pull request 'feature/invoice-labels' (#122) from feature/invoice-labels into development
Reviewed-on: ADUANASOFT/anexo76#122
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CustomsBrokerBaseDTO(BaseModel):
|
||||
@@ -19,7 +19,7 @@ class CustomsBrokerBaseDTO(BaseModel):
|
||||
tax_id: Optional[str] = None
|
||||
personal_id: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
license: Optional[str] = None
|
||||
license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$")
|
||||
company: Optional[str] = None
|
||||
contact: Optional[str] = None
|
||||
|
||||
@@ -27,7 +27,7 @@ class CustomsBrokerBaseDTO(BaseModel):
|
||||
class CustomsBrokerCreateDTO(CustomsBrokerBaseDTO):
|
||||
"""Schema for creating a new CustomsBroker"""
|
||||
|
||||
broker_key: str
|
||||
broker_key: str = Field(..., max_length=5, pattern=r"^[a-zA-Z0-9]+$", description="Clave única del agente aduanal (máx 5 caracteres)")
|
||||
|
||||
|
||||
class CustomsBrokerUpdateDTO(CustomsBrokerBaseDTO):
|
||||
@@ -51,7 +51,7 @@ class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
|
||||
# Legacy DTO for backwards compatibility (if needed elsewhere)
|
||||
class CustomsBrokerDTO(BaseModel):
|
||||
type: Optional[str] = None
|
||||
broker_key: str
|
||||
broker_key: str = Field(..., max_length=5, pattern=r"^[a-zA-Z0-9]+$")
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
postal_code: Optional[str] = None
|
||||
@@ -64,7 +64,7 @@ class CustomsBrokerDTO(BaseModel):
|
||||
tax_id: Optional[str] = None
|
||||
personal_id: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
license: Optional[str] = None
|
||||
license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$")
|
||||
company: Optional[str] = None
|
||||
contact: Optional[str] = None
|
||||
tenant_id: str
|
||||
@@ -100,13 +100,13 @@ class CustomsBrokerVUCreateDTO(BaseModel):
|
||||
|
||||
|
||||
class CustomsBrokerPersonnelDTO(BaseModel):
|
||||
broker_key: str
|
||||
broker_key: str = Field(..., max_length=5, pattern=r"^[a-zA-Z0-9]+$")
|
||||
line: int
|
||||
name: Optional[str]
|
||||
tax_id: Optional[str]
|
||||
personal_id: Optional[str]
|
||||
position: Optional[str]
|
||||
license: Optional[str]
|
||||
license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$")
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
middle_name: Optional[str]
|
||||
|
||||
@@ -117,7 +117,7 @@ export const customsBrokersApi = {
|
||||
* Elimina un agente aduanal
|
||||
*/
|
||||
delete: (brokerKey: string, companyId: string) => {
|
||||
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`);
|
||||
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class PortsApi {
|
||||
const queryParams = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<Port>(`${this.baseUrl}/${id}/?${queryParams.toString()}`);
|
||||
return api.get<Port>(`${this.baseUrl}/${id}?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
async create(data: PortCreate, companyId: string | number): Promise<ApiResponse<Port>> {
|
||||
@@ -78,7 +78,7 @@ class PortsApi {
|
||||
const queryParams = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`${this.baseUrl}/${id}/?${queryParams.toString()}`);
|
||||
return api.delete(`${this.baseUrl}/${id}?${queryParams.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,221 +1,310 @@
|
||||
<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 { Separator } from "$lib/components/ui/separator";
|
||||
import { Loader2 } from "lucide-svelte";
|
||||
import type { CreateCustomsBrokerData, CustomsBroker } from "$lib/api/dashboard/a76/customs-brokers"; // Ajusta la ruta
|
||||
import { toast } from "svelte-sonner";
|
||||
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 { Separator } from '$lib/components/ui/separator';
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
import type {
|
||||
CreateCustomsBrokerData,
|
||||
CustomsBroker
|
||||
} from '$lib/api/dashboard/a76/customs-brokers'; // Ajusta la ruta
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
// --- Props ---
|
||||
export let open = false;
|
||||
export let mode: "create" | "edit" = "create";
|
||||
export let initialData: CustomsBroker | null = null;
|
||||
export let companyId: string; // Necesario según tu API
|
||||
// --- Props ---
|
||||
export let open = false;
|
||||
export let mode: 'create' | 'edit' = 'create';
|
||||
export let initialData: CustomsBroker | null = null;
|
||||
export let companyId: string; // Necesario según tu API
|
||||
|
||||
// La función onSave ahora devuelve una promesa para manejar el loading aquí
|
||||
export let onSave: (data: CreateCustomsBrokerData) => Promise<void>;
|
||||
// La función onSave ahora devuelve una promesa para manejar el loading aquí
|
||||
export let onSave: (data: CreateCustomsBrokerData) => Promise<void>;
|
||||
|
||||
// --- Estado ---
|
||||
let loading = false;
|
||||
// --- Estado ---
|
||||
let loading = false;
|
||||
|
||||
// Estado del formulario
|
||||
let formData: CreateCustomsBrokerData = {
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "",
|
||||
tenant_id: "", // Se llenará en el submit o por defecto
|
||||
company_id: ""
|
||||
};
|
||||
// Estado del formulario
|
||||
let formData: CreateCustomsBrokerData = {
|
||||
broker_key: '',
|
||||
license: '',
|
||||
name: '',
|
||||
tax_id: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
fax: '',
|
||||
contact: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: '',
|
||||
tenant_id: '', // Se llenará en el submit o por defecto
|
||||
company_id: ''
|
||||
};
|
||||
|
||||
// --- Reactividad ---
|
||||
$: if (open) {
|
||||
if (mode === "edit" && initialData) {
|
||||
// Cargar datos existentes
|
||||
formData = {
|
||||
...initialData,
|
||||
// Aseguramos que no sean null/undefined para los inputs
|
||||
name: initialData.name || "",
|
||||
tax_id: initialData.tax_id || "",
|
||||
email: initialData.email || "",
|
||||
phone: initialData.phone || "",
|
||||
fax: initialData.fax || "",
|
||||
contact: initialData.contact || "",
|
||||
address: initialData.address || "",
|
||||
postal_code: initialData.postal_code || "",
|
||||
city: initialData.city || "",
|
||||
state: initialData.state || "",
|
||||
country: initialData.country || "",
|
||||
license: initialData.license || ""
|
||||
};
|
||||
} else {
|
||||
// Reset para crear
|
||||
formData = {
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "MEX", // Valor por defecto sugerido
|
||||
tenant_id: "default", // Ajustar según lógica de tu app
|
||||
company_id: companyId
|
||||
};
|
||||
}
|
||||
}
|
||||
let brokerKeyError = false;
|
||||
let licenseError = false;
|
||||
let brokerKeyTimeout: ReturnType<typeof setTimeout>;
|
||||
let licenseTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// --- Handlers ---
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
// --- Reactividad ---
|
||||
$: if (open) {
|
||||
if (mode === 'edit' && initialData) {
|
||||
// Cargar datos existentes
|
||||
formData = {
|
||||
...initialData,
|
||||
// Aseguramos que no sean null/undefined para los inputs
|
||||
name: initialData.name || '',
|
||||
tax_id: initialData.tax_id || '',
|
||||
email: initialData.email || '',
|
||||
phone: initialData.phone || '',
|
||||
fax: initialData.fax || '',
|
||||
contact: initialData.contact || '',
|
||||
address: initialData.address || '',
|
||||
postal_code: initialData.postal_code || '',
|
||||
city: initialData.city || '',
|
||||
state: initialData.state || '',
|
||||
country: initialData.country || '',
|
||||
license: initialData.license || ''
|
||||
};
|
||||
} else {
|
||||
// Reset para crear
|
||||
formData = {
|
||||
broker_key: '',
|
||||
license: '',
|
||||
name: '',
|
||||
tax_id: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
fax: '',
|
||||
contact: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: 'MEX', // Valor por defecto sugerido
|
||||
tenant_id: 'default', // Ajustar según lógica de tu app
|
||||
company_id: companyId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Validaciones básicas
|
||||
if (!formData.broker_key) {
|
||||
toast.error("La Clave del Agente es obligatoria");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (!formData.license) {
|
||||
toast.error("La Patente es obligatoria");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
// --- Handlers ---
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
// Inyectar company_id si no viene
|
||||
const payload = { ...formData, company_id: companyId };
|
||||
// Validaciones básicas
|
||||
if (!formData.broker_key) {
|
||||
toast.error('La Clave del Agente es obligatoria');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (!formData.license) {
|
||||
toast.error('La Patente es obligatoria');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await onSave(payload);
|
||||
open = false;
|
||||
toast.success(mode === 'create' ? "Agente creado correctamente" : "Agente actualizado correctamente");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Error al guardar el agente aduanal");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
if (formData.broker_key.length > 5) {
|
||||
toast.error('La Clave del Agente no debe superar los 5 caracteres');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (formData.license && formData.license.length > 4) {
|
||||
toast.error('La Patente no debe superar los 4 dígitos');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Inyectar company_id si no viene
|
||||
const payload = { ...formData, company_id: companyId };
|
||||
|
||||
await onSave(payload);
|
||||
open = false;
|
||||
toast.success(
|
||||
mode === 'create' ? 'Agente creado correctamente' : 'Agente actualizado correctamente'
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Error al guardar el agente aduanal');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{mode === "create" ? "Nuevo Agente Aduanal" : "Editar Agente Aduanal"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Ingresa los datos generales del agente. La configuración de VU y Personal se gestiona aparte.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{mode === 'create' ? 'Nuevo Agente Aduanal' : 'Editar Agente Aduanal'}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Ingresa los datos generales del agente. La configuración de VU y Personal se gestiona
|
||||
aparte.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-6 py-4">
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Identificación</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave Agente *</Label>
|
||||
<Input id="broker_key" bind:value={formData.broker_key} placeholder="Ej. 550" disabled={mode === 'edit' || loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente *</Label>
|
||||
<Input id="license" bind:value={formData.license} placeholder="Ej. 3421" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="name">Nombre / Razón Social</Label>
|
||||
<Input id="name" bind:value={formData.name} placeholder="Nombre del Agente o Agencia" disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input id="tax_id" bind:value={formData.tax_id} placeholder="RFC de la agencia" disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Nombre Contacto</Label>
|
||||
<Input id="contact" bind:value={formData.contact} placeholder="Persona de contacto" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-6 py-4">
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Identificación</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave Agente *</Label>
|
||||
<Input
|
||||
id="broker_key"
|
||||
value={formData.broker_key}
|
||||
oninput={(e) => {
|
||||
const val = e.currentTarget.value.toUpperCase();
|
||||
if (val.length > 5) {
|
||||
brokerKeyError = true;
|
||||
formData.broker_key = val.slice(0, 5);
|
||||
e.currentTarget.value = formData.broker_key;
|
||||
|
||||
<Separator />
|
||||
clearTimeout(brokerKeyTimeout);
|
||||
brokerKeyTimeout = setTimeout(() => {
|
||||
brokerKeyError = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
brokerKeyError = false;
|
||||
formData.broker_key = val;
|
||||
}
|
||||
}}
|
||||
placeholder="Ej. 550"
|
||||
maxlength="6"
|
||||
class={brokerKeyError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
disabled={mode === 'edit' || loading}
|
||||
/>
|
||||
{#if brokerKeyError}
|
||||
<p class="text-[0.8rem] font-medium text-destructive">
|
||||
La clave no debe superar los 5 caracteres
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente *</Label>
|
||||
<Input
|
||||
id="license"
|
||||
value={formData.license}
|
||||
oninput={(e) => {
|
||||
const val = e.currentTarget.value.replace(/\D/g, '');
|
||||
if (val.length > 4) {
|
||||
licenseError = true;
|
||||
formData.license = val.slice(0, 4);
|
||||
e.currentTarget.value = formData.license;
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Contacto</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2 col-span-1">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input id="phone" bind:value={formData.phone} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="email">Correo Electrónico</Label>
|
||||
<Input id="email" type="email" bind:value={formData.email} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
clearTimeout(licenseTimeout);
|
||||
licenseTimeout = setTimeout(() => {
|
||||
licenseError = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
licenseError = false;
|
||||
formData.license = val;
|
||||
}
|
||||
}}
|
||||
placeholder="Ej. 3421"
|
||||
maxlength="5"
|
||||
class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
disabled={loading}
|
||||
/>
|
||||
{#if licenseError}
|
||||
<p class="text-[0.8rem] font-medium text-destructive">
|
||||
La patente no debe superar los 4 dígitos
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="name">Nombre / Razón Social</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={formData.name}
|
||||
placeholder="Nombre del Agente o Agencia"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
placeholder="RFC de la agencia"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Nombre Contacto</Label>
|
||||
<Input
|
||||
id="contact"
|
||||
bind:value={formData.contact}
|
||||
placeholder="Persona de contacto"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Dirección Fiscal</h4>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Calle y Número</Label>
|
||||
<Input id="address" bind:value={formData.address} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Contacto</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2 col-span-1">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input id="phone" bind:value={formData.phone} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="email">Correo Electrónico</Label>
|
||||
<Input id="email" type="email" bind:value={formData.email} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">C.P.</Label>
|
||||
<Input id="postal_code" bind:value={formData.postal_code} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input id="city" bind:value={formData.city} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input id="state" bind:value={formData.state} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input id="country" bind:value={formData.country} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Dirección Fiscal</h4>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onclick={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando
|
||||
{:else}
|
||||
Guardar Agente
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Calle y Número</Label>
|
||||
<Input id="address" bind:value={formData.address} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">C.P.</Label>
|
||||
<Input id="postal_code" bind:value={formData.postal_code} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input id="city" bind:value={formData.city} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input id="state" bind:value={formData.state} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input id="country" bind:value={formData.country} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)} disabled={loading}>Cancelar</Button>
|
||||
<Button onclick={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando
|
||||
{:else}
|
||||
Guardar Agente
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
|
||||
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
onRowClick?: (row: TData) => void;
|
||||
selectedId?: string | number | null;
|
||||
idField?: keyof TData;
|
||||
};
|
||||
|
||||
let { data, columns }: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
let { data, columns, onRowClick, selectedId, idField }: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = $derived(
|
||||
createSvelteTable({
|
||||
@@ -45,13 +44,17 @@
|
||||
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row>
|
||||
<Table.Row
|
||||
onclick={() => onRowClick?.(row.original)}
|
||||
class="cursor-pointer transition-colors hover:bg-muted/50 {selectedId &&
|
||||
idField &&
|
||||
row.original[idField] === selectedId
|
||||
? 'bg-muted'
|
||||
: ''}"
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<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 { customsBrokersApi, type CreateCustomsBrokerData, type CustomsBroker } from "$lib/api/dashboard/a76/customs-brokers";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
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 {
|
||||
customsBrokersApi,
|
||||
type CreateCustomsBrokerData,
|
||||
type CustomsBroker
|
||||
} from '$lib/api/dashboard/a76/customs-brokers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -17,56 +21,58 @@
|
||||
} = $props();
|
||||
|
||||
let formData = $state({
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
name: '',
|
||||
type: '',
|
||||
address: '',
|
||||
postal_code: '',
|
||||
city: '',
|
||||
state: '',
|
||||
phone: '',
|
||||
fax: '',
|
||||
email: '',
|
||||
country: '',
|
||||
tax_id: '',
|
||||
personal_id: '',
|
||||
position: '',
|
||||
license: '',
|
||||
company: '',
|
||||
contact: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let licenseError = $state(false);
|
||||
let licenseTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Inicializar formulario cuando cambia el broker
|
||||
$effect(() => {
|
||||
if (open && broker) {
|
||||
formData = {
|
||||
name: broker.name || "",
|
||||
type: broker.type || "",
|
||||
address: broker.address || "",
|
||||
postal_code: broker.postal_code || "",
|
||||
city: broker.city || "",
|
||||
state: broker.state || "",
|
||||
phone: broker.phone || "",
|
||||
fax: broker.fax || "",
|
||||
email: broker.email || "",
|
||||
country: broker.country || "",
|
||||
tax_id: broker.tax_id || "",
|
||||
personal_id: broker.personal_id || "",
|
||||
position: broker.position || "",
|
||||
license: broker.license || "",
|
||||
company: broker.company || "",
|
||||
contact: broker.contact || ""
|
||||
name: broker.name || '',
|
||||
type: broker.type || '',
|
||||
address: broker.address || '',
|
||||
postal_code: broker.postal_code || '',
|
||||
city: broker.city || '',
|
||||
state: broker.state || '',
|
||||
phone: broker.phone || '',
|
||||
fax: broker.fax || '',
|
||||
email: broker.email || '',
|
||||
country: broker.country || '',
|
||||
tax_id: broker.tax_id || '',
|
||||
personal_id: broker.personal_id || '',
|
||||
position: broker.position || '',
|
||||
license: broker.license || '',
|
||||
company: broker.company || '',
|
||||
contact: broker.contact || ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
error = "No hay compañía seleccionada";
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,8 +122,8 @@
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
console.error("Error updating:", e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
console.error('Error updating:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -136,13 +142,17 @@
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Editar Agente Aduanal</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Modifica los datos del agente aduanal <span class="font-mono font-semibold">{broker?.broker_key}</span>.
|
||||
Modifica los datos del agente aduanal <span class="font-mono font-semibold"
|
||||
>{broker?.broker_key}</span
|
||||
>.
|
||||
</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">
|
||||
<div
|
||||
class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -150,7 +160,7 @@
|
||||
<!-- 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="edit-broker_key">Clave</Label>
|
||||
@@ -158,7 +168,7 @@
|
||||
id="edit-broker_key"
|
||||
value={broker?.broker_key}
|
||||
disabled
|
||||
class="bg-muted"
|
||||
class="bg-muted {broker?.broker_key?.length > 5 ? 'border-red-500' : ''}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -190,11 +200,33 @@
|
||||
<Label for="edit-license">Patente</Label>
|
||||
<Input
|
||||
id="edit-license"
|
||||
bind:value={formData.license}
|
||||
value={formData.license}
|
||||
oninput={(e) => {
|
||||
const val = e.currentTarget.value.replace(/\D/g, '');
|
||||
if (val.length > 4) {
|
||||
licenseError = true;
|
||||
formData.license = val.slice(0, 4);
|
||||
e.currentTarget.value = formData.license;
|
||||
|
||||
clearTimeout(licenseTimeout);
|
||||
licenseTimeout = setTimeout(() => {
|
||||
licenseError = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
licenseError = false;
|
||||
formData.license = val;
|
||||
}
|
||||
}}
|
||||
placeholder="Número de patente"
|
||||
maxlength={4}
|
||||
maxlength="5"
|
||||
class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
disabled={loading}
|
||||
/>
|
||||
{#if licenseError}
|
||||
<p class="text-[0.8rem] font-medium text-destructive">
|
||||
La patente no debe superar los 4 dígitos
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@@ -213,7 +245,7 @@
|
||||
<!-- Información de contacto -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información de Contacto</h3>
|
||||
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-phone">Teléfono</Label>
|
||||
@@ -265,7 +297,7 @@
|
||||
<!-- Dirección -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Dirección</h3>
|
||||
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-address">Dirección</Label>
|
||||
<Input
|
||||
@@ -327,7 +359,7 @@
|
||||
<!-- Información fiscal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Fiscal</h3>
|
||||
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-tax_id">RFC</Label>
|
||||
@@ -371,7 +403,9 @@
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"></div>
|
||||
<div
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
|
||||
></div>
|
||||
Guardando...
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Plus, Minus, Search, Loader2, Ghost } from 'lucide-svelte';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { getInvoiceTypeColor } from '$lib/utils';
|
||||
|
||||
let {
|
||||
title = '',
|
||||
@@ -101,7 +102,8 @@
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs"
|
||||
><Badge variant="outline" class="bg-background">{invoice.invoice_type}</Badge
|
||||
><Badge variant="outline" class={getInvoiceTypeColor(invoice.invoice_type)}
|
||||
>{invoice.invoice_type}</Badge
|
||||
></Table.Cell
|
||||
>
|
||||
<Table.Cell>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/in
|
||||
import { createRawSnippet } from "svelte";
|
||||
import DataTableActions from "./data-table-actions.svelte";
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import { getInvoiceTypeColor } from "$lib/utils";
|
||||
|
||||
function formatDate(date?: string | null): string {
|
||||
if (!date) return '-';
|
||||
@@ -70,15 +71,18 @@ export function createColumns(
|
||||
header: "Tipo Factura",
|
||||
cell: ({ row }) => {
|
||||
const invoiceType = row.original.invoice_type;
|
||||
const colorClass = getInvoiceTypeColor(invoiceType);
|
||||
|
||||
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
|
||||
const { type } = getType();
|
||||
const typeSnippet = createRawSnippet<[{ type?: string | null; colorClass: string }]>((getProps) => {
|
||||
const { type, colorClass } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-sm font-medium">${type || '-'}</div>`
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
|
||||
${type || '-'}
|
||||
</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(typeSnippet, { type: invoiceType });
|
||||
return renderSnippet(typeSnippet, { type: invoiceType, colorClass });
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { getInvoiceTypeColor } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
invoice: Invoice;
|
||||
@@ -32,13 +33,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={(v) => { open = v; if (!v) onClose(); }}>
|
||||
<Dialog.Root
|
||||
{open}
|
||||
onOpenChange={(v) => {
|
||||
open = v;
|
||||
if (!v) onClose();
|
||||
}}
|
||||
>
|
||||
<Dialog.Content class="max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Detalles de Factura #{invoice?.id}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Información completa de la factura
|
||||
</Dialog.Description>
|
||||
<Dialog.Description>Información completa de la factura</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if invoice}
|
||||
@@ -74,7 +79,15 @@
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Factura</p>
|
||||
<p class="text-base">{invoice.invoice_type || '-'}</p>
|
||||
<p class="text-base">
|
||||
{#if invoice.invoice_type}
|
||||
<Badge variant="outline" class={getInvoiceTypeColor(invoice.invoice_type)}
|
||||
>{invoice.invoice_type}</Badge
|
||||
>
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -102,10 +115,11 @@
|
||||
<p class="text-base">{invoice.traffic_light_status || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">CFDI UUID</p>
|
||||
<p class="text-xs break-all">{invoice.cfdi_uuid || '-'}</p>
|
||||
</div> <div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">CFDI UUID</p>
|
||||
<p class="text-xs break-all">{invoice.cfdi_uuid || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Actualizado</p>
|
||||
<p class="text-base">{invoice.is_updated ? 'Sí' : 'No'}</p>
|
||||
</div>
|
||||
@@ -196,11 +210,11 @@
|
||||
<p class="text-base">{invoice.compliance_mx.edocument || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Firma Electrónica</p>
|
||||
<p class="text-xs break-all">{invoice.compliance_mx.electronic_signature || '-'}</p>
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Firma Electrónica</p>
|
||||
<p class="text-xs break-all">{invoice.compliance_mx.electronic_signature || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">No hay información de cumplimiento disponible.</p>
|
||||
{/if}
|
||||
@@ -362,7 +376,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Descripción de Colores</p>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Descripción de Colores
|
||||
</p>
|
||||
<p class="text-base">{detail.colors_description || '-'}</p>
|
||||
</div>
|
||||
|
||||
@@ -399,7 +415,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha de Cobranza</p>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Fecha de Cobranza
|
||||
</p>
|
||||
<p class="text-base">{formatDate(collection.collection_date)}</p>
|
||||
</div>
|
||||
|
||||
@@ -427,9 +445,7 @@
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)}>
|
||||
Cerrar
|
||||
</Button>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cerrar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[85vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGO DE PAISES</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -91,11 +91,7 @@
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
<Input bind:value={searchTerm} placeholder="Buscando..." class="flex-1 h-9" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,9 +109,7 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave M3</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Clave M3</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave Mexicana</th
|
||||
>
|
||||
@@ -134,11 +128,11 @@
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(country)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredCountries.length === 0}
|
||||
@@ -152,7 +146,9 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<<
|
||||
|
||||
@@ -24,11 +24,12 @@
|
||||
|
||||
const filteredParts = $derived(
|
||||
searchQuery
|
||||
? parts.filter(p =>
|
||||
p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_english?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
? parts.filter(
|
||||
(p) =>
|
||||
p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_english?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: parts
|
||||
);
|
||||
|
||||
@@ -52,15 +53,12 @@
|
||||
|
||||
isSearching = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const response = await fetch(`/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al buscar números de parte');
|
||||
@@ -87,8 +85,9 @@
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
const threshold = 100;
|
||||
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
const scrolledToBottom =
|
||||
target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
if (scrolledToBottom && displayedParts.length < filteredParts.length) {
|
||||
currentPage++;
|
||||
loadMoreParts();
|
||||
@@ -104,12 +103,10 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[50vw] w-[50vw] max-h-[80vh] flex flex-col">
|
||||
<Dialog.Content class="max-w-5xl w-full max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Número de Parte</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Busca y selecciona un número de parte para la partida
|
||||
</Dialog.Description>
|
||||
<Dialog.Description>Busca y selecciona un número de parte para la partida</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
@@ -148,15 +145,18 @@
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each displayedParts as part}
|
||||
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(part)}>
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
onclick={() => handleSelect(part)}
|
||||
>
|
||||
<Table.Cell class="font-medium">{part.part_number}</Table.Cell>
|
||||
<Table.Cell>{part.description_spanish || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-muted-foreground">{part.description_english || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-muted-foreground"
|
||||
>{part.description_english || '-'}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="text-muted-foreground">{part.part_class || '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
Seleccionar
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-8">Seleccionar</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let loadingMore = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
|
||||
// Pagination state
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(1);
|
||||
@@ -34,7 +34,7 @@
|
||||
loadingMore = true;
|
||||
}
|
||||
error = '';
|
||||
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
@@ -45,11 +45,11 @@
|
||||
const response = await fetch(`/api-sveltekit/tariff-fractions?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Tariff fractions data received:', data);
|
||||
|
||||
|
||||
if (data.items && Array.isArray(data.items)) {
|
||||
if (append) {
|
||||
fractions = [...fractions, ...data.items];
|
||||
@@ -79,10 +79,10 @@
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
if (!scrollContainer || loading || loadingMore || !hasMore) return;
|
||||
|
||||
|
||||
const target = e.target as HTMLDivElement;
|
||||
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||
|
||||
|
||||
// Load more when within 200px of bottom
|
||||
if (scrollBottom < 200) {
|
||||
loadFractions(currentPage + 1, true);
|
||||
@@ -120,7 +120,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[80vw] w-[80vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[80vh] flex flex-col px-6">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">FRACCIONES ARANCELARIAS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -139,11 +139,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
onscroll={handleScroll}
|
||||
class="flex-1 overflow-auto px-6 py-4"
|
||||
>
|
||||
<div bind:this={scrollContainer} onscroll={handleScroll} class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
@@ -157,18 +153,12 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Código</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Fracción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Código</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Fracción</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>NICO</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">NICO</th>
|
||||
<th class="px-3 py-2 text-left font-semibold">UMT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -204,9 +194,7 @@
|
||||
{/if}
|
||||
|
||||
{#if !hasMore && fractions.length > 0}
|
||||
<div class="text-center py-4 text-sm text-zinc-500">
|
||||
Todos los resultados cargados
|
||||
</div>
|
||||
<div class="text-center py-4 text-sm text-zinc-500">Todos los resultados cargados</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGOS DE UNIDADES DE MEDIDA</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -92,11 +92,7 @@
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
<Input bind:value={searchTerm} placeholder="Buscando..." class="flex-1 h-9" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,9 +110,7 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>U.M.</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">U.M.</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción Español</th
|
||||
>
|
||||
@@ -157,7 +151,9 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<<
|
||||
|
||||
@@ -36,6 +36,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Port>[] {
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { deletePort, type Port } from "$lib/api/dashboard/a76/general_catalogs/ports";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import type { Port } from "$lib/api/dashboard/a76/general_catalogs/ports";
|
||||
import { EllipsisVertical, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from "./create-edit-dialog.svelte";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import DeleteDialog from "./delete-dialog.svelte";
|
||||
|
||||
let {
|
||||
item,
|
||||
@@ -14,42 +14,8 @@
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm(`¿Estás seguro de eliminar el puerto "${item.port_code}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('Selecciona una compañía antes de eliminar');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await deletePort(item.id, companyId);
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
alert(`Error al eliminar: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = 'Error de conexión';
|
||||
console.error(e);
|
||||
alert('Error de conexión al eliminar');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
let editDialogOpen = $state(false);
|
||||
let deleteDialogOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
@@ -63,24 +29,26 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={() => dialogOpen = true}>
|
||||
<DropdownMenu.Item onclick={() => editDialogOpen = true}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
<DropdownMenu.Item onclick={() => deleteDialogOpen = true} class="text-destructive">
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
bind:open={editDialogOpen}
|
||||
mode="edit"
|
||||
{item}
|
||||
{onSuccess}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
bind:open={deleteDialogOpen}
|
||||
{item}
|
||||
{onSuccess}
|
||||
/>
|
||||
|
||||
117
frontend/src/lib/components/dashboard/ports/delete-dialog.svelte
Normal file
117
frontend/src/lib/components/dashboard/ports/delete-dialog.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { portsApi, type Port } from '$lib/api/dashboard/a76/general_catalogs/ports';
|
||||
import { LoaderCircle } from 'lucide-svelte';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item: Port | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!item) return;
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'Selecciona una compañía antes de eliminar';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await portsApi.delete(item.id, companyId);
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
open = false;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al eliminar';
|
||||
console.error('Error deleting:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
error = null;
|
||||
}
|
||||
open = newOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
|
||||
<AlertDialog.Description class="space-y-2">
|
||||
<p>Esta acción no se puede deshacer. Se eliminará permanentemente este puerto:</p>
|
||||
{#if item}
|
||||
<div class="mt-2 rounded-lg bg-muted p-3 space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Código:</span>
|
||||
<code class="font-mono font-semibold">{item.port_code}</code>
|
||||
</div>
|
||||
{#if item.description}
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<span class="font-medium">Descripción:</span>
|
||||
<span class="text-xs">{item.description}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.location_code}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Ubicación:</span>
|
||||
<code class="font-mono font-semibold">{item.location_code}</code>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2 rounded-lg bg-muted p-3 text-sm italic">
|
||||
Cargando información del puerto...
|
||||
</div>
|
||||
{/if}
|
||||
{#if error}
|
||||
<div
|
||||
class="mt-2 rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel disabled={loading}>Cancelar</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
onclick={handleDelete}
|
||||
disabled={loading}
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
Eliminando...
|
||||
{:else}
|
||||
Eliminar
|
||||
{/if}
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
@@ -12,19 +12,19 @@ export function cn(...inputs: ClassValue[]) {
|
||||
*/
|
||||
export function getBackendAssetUrl(path: string | null | undefined): string {
|
||||
if (!path) return '';
|
||||
|
||||
|
||||
// Si ya es una URL completa, retornarla tal cual
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
// Eliminar la / inicial si existe para evitar //
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
|
||||
|
||||
// Obtener la base URL del API y limpiar el / final si existe
|
||||
let baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
baseUrl = baseUrl.replace(/\/+$/, ''); // Eliminar todas las / del final
|
||||
|
||||
|
||||
return `${baseUrl}/${cleanPath}`;
|
||||
}
|
||||
|
||||
@@ -34,3 +34,39 @@ export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
||||
/**
|
||||
* Obtiene el color del badge según el tipo de factura (SCAII Standards)
|
||||
*/
|
||||
export function getInvoiceTypeColor(type?: string | null): string {
|
||||
if (!type) return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
|
||||
|
||||
const normalizedType = type.toLowerCase().trim();
|
||||
|
||||
// Impo tem (Rojo)
|
||||
if (normalizedType.includes('impo tem') || normalizedType === 'tem') {
|
||||
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
// Impo Def (Verde)
|
||||
if (normalizedType.includes('impo def') || normalizedType === 'def') {
|
||||
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||
}
|
||||
// Comp Mex (Morado)
|
||||
if (normalizedType.includes('comp mex') || normalizedType === 'mex') {
|
||||
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
|
||||
}
|
||||
// Cam. Reg. (Azul)
|
||||
if (normalizedType.includes('cam. reg.') || normalizedType.includes('cam reg') || normalizedType === 'cr') {
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
}
|
||||
// Expo. (Azul)
|
||||
if (normalizedType.includes('expo') || normalizedType === 'exdef' || normalizedType === 'pterm') {
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
}
|
||||
// Imp. Rep. (Azul Claro)
|
||||
if (normalizedType.includes('imp. rep.') || normalizedType.includes('imp rep') || normalizedType === 'repar') {
|
||||
return 'bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-200';
|
||||
}
|
||||
|
||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200';
|
||||
}
|
||||
|
||||
@@ -18,10 +18,15 @@
|
||||
customsSectionsApi,
|
||||
type CustomsSection
|
||||
} from '$lib/api/dashboard/reference_data/customs_sections';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/customs_sections/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/customs_sections/columns.js';
|
||||
import SectionsDataTable from '$lib/components/dashboard/reference_data/customs_sections/data-table.svelte';
|
||||
import { createColumns as createSectionColumns } from '$lib/components/dashboard/reference_data/customs_sections/columns.js';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
|
||||
// Specialized Broker Components
|
||||
import BrokerDataTable from '$lib/components/dashboard/customs_brokers/data-table.svelte';
|
||||
import { createColumns as createBrokerColumns } from '$lib/components/dashboard/customs_brokers/columns';
|
||||
import DeleteDialog from '$lib/components/dashboard/customs_brokers/delete-dialog.svelte';
|
||||
|
||||
let { data }: { data: any } = $props();
|
||||
|
||||
// Global State
|
||||
@@ -31,6 +36,7 @@
|
||||
let items = $state<CustomsBroker[]>(data.items || []);
|
||||
let selectedItem = $state<CustomsBroker | null>(null);
|
||||
let isLoading = $state(false);
|
||||
let showDeleteDialog = $state(false);
|
||||
|
||||
// Server-side filtering/pagination (for Brokers)
|
||||
let allItemsRaw = $state<CustomsBroker[]>(data.brokers || []);
|
||||
@@ -114,18 +120,13 @@
|
||||
}
|
||||
async function handleDelete() {
|
||||
if (!selectedItem || !companyStore.activeCompany?.id) return;
|
||||
if (!confirm('¿Estás seguro de eliminar este Agente Aduanal?')) return;
|
||||
try {
|
||||
await customsBrokersApi.delete(
|
||||
selectedItem.broker_key,
|
||||
companyStore.activeCompany.id.toString()
|
||||
);
|
||||
toast.success('Agente eliminado');
|
||||
selectedItem = null;
|
||||
loadItems();
|
||||
} catch (e) {
|
||||
toast.error('Error al eliminar');
|
||||
}
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
function handleActionSuccess() {
|
||||
loadItems();
|
||||
selectedItem = null;
|
||||
toast.success('Cambios aplicados correctamente');
|
||||
}
|
||||
|
||||
// Customs Sections Actions
|
||||
@@ -178,10 +179,12 @@
|
||||
})
|
||||
);
|
||||
|
||||
const sectionsColumns = createColumns(() => {
|
||||
const sectionsColumns = createSectionColumns(() => {
|
||||
sectionsPage = 1;
|
||||
loadSections();
|
||||
});
|
||||
|
||||
const brokerColumns = createBrokerColumns(handleActionSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-[calc(100vh-4rem)] p-4 gap-4 pb-15">
|
||||
@@ -264,45 +267,13 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-card">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted text-muted-foreground border-b">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left w-24">Patente</th>
|
||||
<th class="px-3 py-2 text-left">Nombre</th>
|
||||
<th class="px-3 py-2 text-left">Licencia</th>
|
||||
<th class="px-3 py-2 text-left">Ciudad</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if isLoading}
|
||||
<tr
|
||||
><td colspan="4" class="text-center py-8 text-muted-foreground">Cargando...</td
|
||||
></tr
|
||||
>
|
||||
{:else if paginatedItems.length === 0}
|
||||
<tr
|
||||
><td colspan="4" class="text-center py-8 text-muted-foreground"
|
||||
>No se encontraron registros</td
|
||||
></tr
|
||||
>
|
||||
{:else}
|
||||
{#each paginatedItems as item (item.broker_key)}
|
||||
<tr
|
||||
class="border-b cursor-pointer transition-colors hover:bg-muted/50 {selectedItem?.broker_key ===
|
||||
item.broker_key
|
||||
? 'bg-muted'
|
||||
: ''}"
|
||||
onclick={() => selectItem(item)}
|
||||
>
|
||||
<td class="px-3 py-2 font-mono font-bold">{item.broker_key}</td>
|
||||
<td class="px-3 py-2 font-medium">{item.name || '-'}</td>
|
||||
<td class="px-3 py-2 text-muted-foreground">{item.license || '-'}</td>
|
||||
<td class="px-3 py-2 text-muted-foreground">{item.city || '-'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<BrokerDataTable
|
||||
data={paginatedItems}
|
||||
columns={brokerColumns}
|
||||
onRowClick={selectItem}
|
||||
selectedId={selectedItem?.broker_key}
|
||||
idField="broker_key"
|
||||
/>
|
||||
</div>
|
||||
<!-- Simple Pagination Controls -->
|
||||
{#if totalItems > pageSize}
|
||||
@@ -434,7 +405,7 @@
|
||||
<!-- Reusing DataTable from Customs Sections -->
|
||||
<Card.Root class="h-full flex flex-col border-none shadow-none">
|
||||
<Card.Content class="flex-1 p-0">
|
||||
<DataTable
|
||||
<SectionsDataTable
|
||||
data={sections}
|
||||
columns={sectionsColumns}
|
||||
loading={loadingSections}
|
||||
@@ -477,3 +448,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeleteDialog bind:open={showDeleteDialog} broker={selectedItem} onSuccess={handleActionSuccess} />
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
company_id: ''
|
||||
});
|
||||
|
||||
let brokerKeyError = $state(false);
|
||||
let licenseError = $state(false);
|
||||
let brokerKeyTimeout: ReturnType<typeof setTimeout>;
|
||||
let licenseTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// --- 3. CARGA DE DATOS REACTIVA ---
|
||||
$effect(() => {
|
||||
const company = companyStore.activeCompany;
|
||||
@@ -126,6 +131,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.broker_key.length > 5) {
|
||||
error = 'La Clave del Agente no debe superar los 5 caracteres';
|
||||
toast.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.license && formData.license.length > 4) {
|
||||
error = 'La Patente no debe superar los 4 dígitos';
|
||||
toast.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
@@ -213,19 +230,66 @@
|
||||
>Clave Agente <span class="text-destructive">*</span></Label
|
||||
>
|
||||
<Input
|
||||
bind:value={formData.broker_key}
|
||||
value={formData.broker_key}
|
||||
oninput={(e) => {
|
||||
const val = e.currentTarget.value.toUpperCase();
|
||||
if (val.length > 5) {
|
||||
brokerKeyError = true;
|
||||
formData.broker_key = val.slice(0, 5);
|
||||
e.currentTarget.value = formData.broker_key;
|
||||
|
||||
clearTimeout(brokerKeyTimeout);
|
||||
brokerKeyTimeout = setTimeout(() => {
|
||||
brokerKeyError = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
brokerKeyError = false;
|
||||
formData.broker_key = val;
|
||||
}
|
||||
}}
|
||||
placeholder="Ej. 550"
|
||||
maxlength="6"
|
||||
class={brokerKeyError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
disabled={isEdit || loading}
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Clave interna o número de patente único.
|
||||
</p>
|
||||
{#if brokerKeyError}
|
||||
<p class="text-[0.8rem] font-medium text-destructive">
|
||||
La clave no debe superar los 5 caracteres
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label class="required"
|
||||
>Patente / Autorización <span class="text-destructive">*</span></Label
|
||||
>
|
||||
<Input bind:value={formData.license} placeholder="Ej. 3421" disabled={loading} />
|
||||
<Input
|
||||
value={formData.license}
|
||||
oninput={(e) => {
|
||||
const val = e.currentTarget.value.replace(/\D/g, '');
|
||||
if (val.length > 4) {
|
||||
licenseError = true;
|
||||
formData.license = val.slice(0, 4);
|
||||
e.currentTarget.value = formData.license;
|
||||
|
||||
clearTimeout(licenseTimeout);
|
||||
licenseTimeout = setTimeout(() => {
|
||||
licenseError = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
licenseError = false;
|
||||
formData.license = val;
|
||||
}
|
||||
}}
|
||||
placeholder="Ej. 3421"
|
||||
maxlength="5"
|
||||
class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
disabled={loading}
|
||||
/>
|
||||
{#if licenseError}
|
||||
<p class="text-[0.8rem] font-medium text-destructive">
|
||||
La patente no debe superar los 4 dígitos
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -660,7 +660,7 @@
|
||||
|
||||
<!-- Dialog para Insertar/Editar Clase de Activo Fijo -->
|
||||
<Dialog.Root bind:open={showInsertDialog}>
|
||||
<Dialog.Content class="!max-w-[1600px] !w-[1600px] !h-[90vh] p-0 overflow-hidden flex flex-col">
|
||||
<Dialog.Content class="max-w-6xl w-full h-[90vh] p-0 overflow-hidden flex flex-col">
|
||||
<Dialog.Header class="p-6 pb-4 border-b">
|
||||
<Dialog.Title>{selectedClass ? 'Editar' : 'Nueva'} Clase de Activo Fijo</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
Reference in New Issue
Block a user