Se termino de agregar los formularios modales a los catalogos que faltaba
This commit is contained in:
@@ -23,9 +23,8 @@
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Tipo de Cambio" : "Nuevo Tipo de Cambio");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
date: '', // Se usará con input type="date" (YYYY-MM-DD)
|
||||
date: '',
|
||||
value: null as number | null,
|
||||
local_currency: '',
|
||||
foreign_currency: ''
|
||||
@@ -38,10 +37,7 @@
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item && item.date) {
|
||||
// Truco: Si la fecha viene con hora (ej: 2023-01-01T12:00:00),
|
||||
// solo tomamos la parte de la fecha para el input.
|
||||
const formattedDate = item.date.includes('T') ? item.date.split('T')[0] : item.date;
|
||||
|
||||
formData = {
|
||||
date: formattedDate,
|
||||
value: item.value,
|
||||
@@ -49,12 +45,11 @@
|
||||
foreign_currency: item.foreign_currency || ''
|
||||
};
|
||||
} else {
|
||||
// Reset para nuevo registro. Ponemos la fecha de hoy por default.
|
||||
formData = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
value: null,
|
||||
local_currency: 'MXN', // Default común
|
||||
foreign_currency: 'USD' // Default común
|
||||
local_currency: 'MXN',
|
||||
foreign_currency: 'USD'
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
@@ -64,40 +59,30 @@
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones básicas
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (formData.value === null || formData.value === undefined) throw new Error('El valor es requerido');
|
||||
if (formData.value === null) throw new Error('El valor es requerido');
|
||||
|
||||
// Preparar datos
|
||||
// Pydantic suele aceptar YYYY-MM-DD para campos datetime sin problema.
|
||||
const dataToSend = {
|
||||
date: formData.date,
|
||||
value: Number(formData.value),
|
||||
// Estandarizamos a mayúsculas las monedas
|
||||
local_currency: formData.local_currency?.trim().toUpperCase() || null,
|
||||
foreign_currency: formData.foreign_currency?.trim().toUpperCase() || null
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
// 👇 companyId va como tercer argumento, ¡bien ahí!
|
||||
if (isEdit && item) {
|
||||
response = await updateExchangeRate(item.id, dataToSend, companyId);
|
||||
await updateExchangeRate(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
response = await createExchangeRate(dataToSend, companyId);
|
||||
await createExchangeRate(dataToSend, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
// Si el API wrapper no lanza error, revisa cómo manejar la respuesta de error aquí
|
||||
error = e instanceof Error ? e.message : 'Error al guardar el tipo de cambio';
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -105,83 +90,60 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[500px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="date" class="text-right">Fecha <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
bind:value={formData.date}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="date" class="text-right">Fecha *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="date" type="date" bind:value={formData.date} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="value" type="number" step="0.000001" bind:value={formData.value} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="local_currency" class="text-right">Local</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="local_currency" bind:value={formData.local_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="foreign_currency" class="text-right">Extranjera</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="foreign_currency" bind:value={formData.foreign_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="value"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.value}
|
||||
placeholder="Ej: 18.5000"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Hasta 6 decimales.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="local_currency" class="text-right">Moneda Local</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="local_currency"
|
||||
bind:value={formData.local_currency}
|
||||
placeholder="Ej: MXN"
|
||||
maxlength={7}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="foreign_currency" class="text-right">Moneda Ext.</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="foreign_currency"
|
||||
bind:value={formData.foreign_currency}
|
||||
placeholder="Ej: USD"
|
||||
maxlength={7}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,265 @@
|
||||
<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 * as Tabs from "$lib/components/ui/tabs"; // Necesario para organizar
|
||||
import { Plus, Trash2 } from "lucide-svelte"; // Iconos para la lista
|
||||
|
||||
import {
|
||||
createDoda,
|
||||
updateDoda,
|
||||
type Doda,
|
||||
type DodaContainer,
|
||||
type DodaPedimento,
|
||||
type DodaAmericanPedimento
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
doda = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
doda?: Doda | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!doda);
|
||||
const title = $derived(isEdit ? `Editar DODA ${doda?.integration_number || ''}` : "Nuevo DODA");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
integration_number: '',
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
// Arrays
|
||||
containers: [] as Partial<DodaContainer>[],
|
||||
pedimentos_detail: [] as Partial<DodaPedimento>[],
|
||||
american_pedimentos: [] as Partial<DodaAmericanPedimento>[]
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Helpers
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Manejo de Arrays
|
||||
function addContainer() {
|
||||
formData.containers = [...formData.containers, { container_value: '', seals: '' }];
|
||||
}
|
||||
|
||||
function removeContainer(index: number) {
|
||||
formData.containers = formData.containers.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (doda) {
|
||||
formData = {
|
||||
integration_number: doda.integration_number || '',
|
||||
dispatch_customs: doda.dispatch_customs || '',
|
||||
customs_sections: doda.customs_sections || '',
|
||||
patent: doda.patent || '',
|
||||
caat: doda.caat || '',
|
||||
transport_identification: doda.transport_identification || '',
|
||||
fast_id: doda.fast_id || '',
|
||||
operation_type: doda.operation_type || '',
|
||||
containers: doda.containers || [],
|
||||
pedimentos_detail: doda.pedimentos_detail || [],
|
||||
american_pedimentos: doda.american_pedimentos || []
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
integration_number: '',
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
containers: [],
|
||||
pedimentos_detail: [],
|
||||
american_pedimentos: []
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Nota: Aquí validaciones si fueran necesarias
|
||||
|
||||
// Fix para el ID: Usamos id o sys_id según venga
|
||||
const idToUpdate = doda?.sys_id || doda?.id;
|
||||
|
||||
if (isEdit && idToUpdate) {
|
||||
await updateDoda(idToUpdate, formData);
|
||||
} else {
|
||||
await createDoda(formData);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar DODA';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="py-2">
|
||||
{#if error}
|
||||
<div class="mb-4 rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-3">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Transporte</Tabs.Trigger>
|
||||
<Tabs.Trigger value="containers">
|
||||
Contenedores ({formData.containers.length})
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="general" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="integration_number">No. Integración</Label>
|
||||
<Input id="integration_number" bind:value={formData.integration_number} maxlength={30} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="operation_type">Tipo Operación</Label>
|
||||
<select
|
||||
id="operation_type"
|
||||
bind:value={formData.operation_type}
|
||||
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"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
<option value="1">Importación</option>
|
||||
<option value="2">Exportación</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="transport" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat">CAAT</Label>
|
||||
<Input id="caat" bind:value={formData.caat} maxlength={10} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transport_id">Identificación Transporte</Label>
|
||||
<Input id="transport_id" bind:value={formData.transport_identification} maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fast_id">FAST ID</Label>
|
||||
<Input id="fast_id" bind:value={formData.fast_id} maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="containers" class="space-y-4 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<Label>Lista de Contenedores</Label>
|
||||
<Button type="button" size="sm" variant="outline" onclick={addContainer} disabled={loading}>
|
||||
<Plus class="mr-2 h-3 w-3" /> Agregar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 max-h-[300px] overflow-y-auto pr-1">
|
||||
{#if formData.containers.length === 0}
|
||||
<div class="rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground">
|
||||
No hay contenedores registrados.
|
||||
</div>
|
||||
{:else}
|
||||
{#each formData.containers as container, i}
|
||||
<div class="flex items-end gap-3 rounded-md border p-3 bg-muted/20">
|
||||
<div class="grid gap-1.5 flex-1">
|
||||
<Label class="text-xs">Valor Contenedor</Label>
|
||||
<Input bind:value={container.container_value} placeholder="Ej. ABCD123456" class="h-8" />
|
||||
</div>
|
||||
<div class="grid gap-1.5 flex-1">
|
||||
<Label class="text-xs">Candados</Label>
|
||||
<Input bind:value={container.seals} placeholder="Separados por coma" class="h-8" />
|
||||
</div>
|
||||
<Button type="button" variant="destructive" size="icon" class="h-8 w-8" onclick={() => removeContainer(i)}>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
{#if isEdit && doda}
|
||||
<div class="mt-4 rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(doda.created_at)}</span>
|
||||
</div>
|
||||
{#if doda.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(doda.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer class="mt-6">
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,209 @@
|
||||
<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 {
|
||||
createElectronicNotice,
|
||||
updateElectronicNotice,
|
||||
type ElectronicNotice
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
notice = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
notice?: ElectronicNotice | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!notice);
|
||||
const title = $derived(isEdit ? "Editar Aviso Electrónico" : "Nuevo Aviso Electrónico");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
notice_number: '',
|
||||
year: '',
|
||||
patent: '',
|
||||
pedimento: '',
|
||||
invoice: '',
|
||||
status: '',
|
||||
validation_acknowledgment: '',
|
||||
certificate_number: '',
|
||||
file_sent: '',
|
||||
file_response: '',
|
||||
fea: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (notice) {
|
||||
formData = {
|
||||
notice_number: notice.notice_number || '',
|
||||
year: notice.year || '',
|
||||
patent: notice.patent || '',
|
||||
pedimento: notice.pedimento || '',
|
||||
invoice: notice.invoice || '',
|
||||
status: notice.status || '',
|
||||
validation_acknowledgment: notice.validation_acknowledgment || '',
|
||||
certificate_number: notice.certificate_number || '',
|
||||
file_sent: notice.file_sent || '',
|
||||
file_response: notice.file_response || '',
|
||||
fea: notice.fea || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
notice_number: '', year: '', patent: '', pedimento: '', invoice: '',
|
||||
status: '', validation_acknowledgment: '', certificate_number: '',
|
||||
file_sent: '', file_response: '', fea: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones básicas
|
||||
if (!formData.notice_number.trim()) throw new Error('El número de aviso es requerido');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
...formData,
|
||||
notice_number: formData.notice_number.trim(),
|
||||
year: formData.year.trim(),
|
||||
patent: formData.patent.trim(),
|
||||
pedimento: formData.pedimento.trim()
|
||||
};
|
||||
|
||||
if (isEdit && notice) {
|
||||
await updateElectronicNotice(notice.id, dataToSend);
|
||||
} else {
|
||||
await createElectronicNotice(dataToSend);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar el aviso';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
<div class="col-span-2 border-b pb-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Identificación</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="notice_number">No. Aviso <span class="text-destructive">*</span></Label>
|
||||
<Input id="notice_number" bind:value={formData.notice_number} placeholder="Ej. AV-2025-001" maxlength={500} disabled={loading} required />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="year">Año</Label>
|
||||
<Input id="year" bind:value={formData.year} placeholder="Ej. 2025" maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 border-b pb-2 mt-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Datos Operativos</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} placeholder="Ej. 1234" maxlength={4} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento">Pedimento</Label>
|
||||
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Ej. 5000123" maxlength={15} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="invoice">Factura / Invoice</Label>
|
||||
<Input id="invoice" bind:value={formData.invoice} maxlength={50} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} placeholder="Ej. VALIDADO" maxlength={100} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 border-b pb-2 mt-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Validación</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="val_ack">Acuse Validación</Label>
|
||||
<Input id="val_ack" bind:value={formData.validation_acknowledgment} maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="cert">No. Certificado</Label>
|
||||
<Input id="cert" bind:value={formData.certificate_number} maxlength={50} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isEdit && notice}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(notice.created_at)}</span>
|
||||
</div>
|
||||
{#if notice.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(notice.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,141 +1,165 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import {
|
||||
createErrorClassification,
|
||||
updateErrorClassification,
|
||||
type ErrorClassification
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/error-catalogs'; // Ajusta la ruta
|
||||
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 {
|
||||
createErrorClassification,
|
||||
updateErrorClassification,
|
||||
type ErrorClassification
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/error-catalogs'; // Ajusta la ruta si es necesario
|
||||
|
||||
export let open: boolean = false;
|
||||
export let classification: ErrorClassification | null = null;
|
||||
let {
|
||||
open = $bindable(false),
|
||||
classification = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
classification?: ErrorClassification | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let loading = false;
|
||||
const isEdit = $derived(!!classification);
|
||||
const title = $derived(isEdit ? "Editar Clasificación" : "Nueva Clasificación");
|
||||
|
||||
let formData = {
|
||||
code: '',
|
||||
level: ''
|
||||
};
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
level: ''
|
||||
});
|
||||
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$: if (classification) {
|
||||
formData = {
|
||||
code: classification.code,
|
||||
level: classification.level || ''
|
||||
};
|
||||
} else {
|
||||
formData = { code: '', level: '' };
|
||||
}
|
||||
// Función auxiliar para fechas (solo visualización)
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
try {
|
||||
if (classification?.id) {
|
||||
await updateErrorClassification(classification.id, { level: formData.level });
|
||||
} else {
|
||||
await createErrorClassification(formData);
|
||||
}
|
||||
dispatch('save');
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error al guardar');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// Cargar datos al abrir o cambiar el item
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (classification) {
|
||||
formData = {
|
||||
code: classification.code,
|
||||
level: classification.level || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
level: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
open = false;
|
||||
dispatch('close');
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones básicas
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
level: formData.level.trim()
|
||||
};
|
||||
|
||||
if (isEdit && classification) {
|
||||
// En edición, solo mandamos el nivel según tu lógica original (código bloqueado)
|
||||
await updateErrorClassification(classification.id, { level: dataToSend.level });
|
||||
} else {
|
||||
await createErrorClassification(dataToSend);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la clasificación';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||
|
||||
<div class="w-full max-w-md rounded-xl bg-[#1a1a1a] border border-gray-700 shadow-2xl overflow-hidden">
|
||||
|
||||
<div class="bg-[#1a1a1a] px-6 py-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-bold text-white">
|
||||
{classification ? 'Editar Clasificación' : 'Nueva Clasificación'}
|
||||
</h3>
|
||||
<button on:click={closeModal} class="text-gray-400 hover:text-white transition-colors text-2xl">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[450px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="p-6 space-y-5">
|
||||
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-300 mb-1">Código *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
required
|
||||
maxlength="100"
|
||||
disabled={!!classification}
|
||||
class="w-full rounded-lg bg-[#252525] border border-gray-600 text-white p-2.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed placeholder-gray-500"
|
||||
placeholder="Ej. SYSTEM_ERROR"
|
||||
/>
|
||||
{#if classification}
|
||||
<p class="text-xs text-gray-500 mt-1">El código no se puede cambiar.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="level" class="block text-sm font-medium text-gray-300 mb-1">Nivel</label>
|
||||
<input
|
||||
type="text"
|
||||
id="level"
|
||||
bind:value={formData.level}
|
||||
maxlength="3"
|
||||
class="w-full rounded-lg bg-[#252525] border border-gray-600 text-white p-2.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-gray-500"
|
||||
placeholder="Ej. CRT"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej. SYSTEM_ERROR"
|
||||
maxlength={100}
|
||||
disabled={isEdit || loading}
|
||||
required
|
||||
/>
|
||||
{#if isEdit}
|
||||
<p class="text-[10px] text-muted-foreground mt-1">El código no se puede modificar.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if classification}
|
||||
<div class="mt-4 p-3 rounded bg-gray-800/50 border border-gray-700 text-xs text-gray-400 space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span>Creado:</span>
|
||||
<span class="text-gray-300">{formatDate(classification.created_at)}</span>
|
||||
</div>
|
||||
{#if classification.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span>Actualizado:</span>
|
||||
<span class="text-gray-300">{formatDate(classification.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="level" class="text-right">Nivel</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="level"
|
||||
bind:value={formData.level}
|
||||
placeholder="Ej. CRT"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 3 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-700 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={closeModal}
|
||||
class="px-4 py-2 text-sm font-medium text-gray-300 bg-transparent border border-gray-600 rounded-lg hover:bg-gray-800 hover:text-white transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-900/20"
|
||||
>
|
||||
{loading ? 'Guardando...' : (classification ? 'Guardar Cambios' : 'Crear')}
|
||||
</button>
|
||||
</div>
|
||||
{#if isEdit && classification}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(classification.created_at)}</span>
|
||||
</div>
|
||||
{#if classification.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(classification.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -23,7 +23,6 @@
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar INPC" : "Nuevo INPC");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
year: '',
|
||||
month: '',
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from '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";
|
||||
// Si tienes el componente Textarea impórtalo, si no, usa la etiqueta html con clases
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
|
||||
import {
|
||||
createLocation,
|
||||
updateLocation,
|
||||
type Location
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/locations';
|
||||
|
||||
// Props
|
||||
export let open: boolean = false;
|
||||
export let location: Location | null = null;
|
||||
let {
|
||||
open = $bindable(false),
|
||||
location = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
location?: Location | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let loading = false;
|
||||
const isEdit = $derived(!!location);
|
||||
const title = $derived(isEdit ? "Editar Ubicación" : "Nueva Ubicación");
|
||||
|
||||
// Form Data
|
||||
let formData = {
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: ''
|
||||
};
|
||||
});
|
||||
|
||||
// Función para formatear fechas (auditoría)
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
@@ -28,125 +43,124 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Reactividad: Cargar datos si estamos editando
|
||||
$: if (location) {
|
||||
formData = {
|
||||
code: location.code,
|
||||
description: location.description || ''
|
||||
};
|
||||
} else {
|
||||
// Limpiar si es nuevo
|
||||
formData = { code: '', description: '' };
|
||||
}
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (location) {
|
||||
formData = {
|
||||
code: location.code,
|
||||
description: location.description || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
if (location?.id) {
|
||||
// EDITAR
|
||||
await updateLocation(location.id, {
|
||||
code: formData.code,
|
||||
description: formData.description
|
||||
});
|
||||
// Validaciones
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
if (formData.code.length > 5) throw new Error('El código no puede tener más de 5 caracteres');
|
||||
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
description: formData.description.trim()
|
||||
};
|
||||
|
||||
if (isEdit && location) {
|
||||
await updateLocation(location.id, dataToSend);
|
||||
} else {
|
||||
// CREAR
|
||||
await createLocation({
|
||||
code: formData.code,
|
||||
description: formData.description
|
||||
});
|
||||
await createLocation(dataToSend);
|
||||
}
|
||||
dispatch('success'); // Avisamos al padre para que recargue
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error('Error guardando location:', error);
|
||||
// Aquí puedes poner un toast de error si tienes
|
||||
alert('Error al guardar la localización.');
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la ubicación';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
open = false;
|
||||
dispatch('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
||||
|
||||
<div class="w-full max-w-md rounded-xl bg-[#1a1a1a] border border-gray-700 shadow-2xl overflow-hidden">
|
||||
|
||||
<div class="bg-[#1a1a1a] px-6 py-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-bold text-white">
|
||||
{location ? 'Editar Ubicación' : 'Nueva Ubicación'}
|
||||
</h3>
|
||||
<button on:click={closeModal} class="text-gray-400 hover:text-white transition-colors text-2xl">
|
||||
×
|
||||
</button>
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[450px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej. VER"
|
||||
maxlength={5}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 5 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label for="description" class="text-right pt-2">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Descripción de la ubicación..."
|
||||
maxlength={200}
|
||||
disabled={loading}
|
||||
class="resize-none min-h-[80px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="p-6 space-y-5">
|
||||
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-300 mb-1">Código *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
required
|
||||
maxlength="5"
|
||||
class="w-full rounded-lg bg-[#252525] border border-gray-600 text-white p-2.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-gray-500"
|
||||
placeholder="Ej. VER"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1 text-right">Máx. 5 caracteres</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-300 mb-1">Descripción</label>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
rows="3"
|
||||
maxlength="200"
|
||||
class="w-full rounded-lg bg-[#252525] border border-gray-600 text-white p-2.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-gray-500 resize-none"
|
||||
placeholder="Descripción de la ubicación..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{#if location}
|
||||
<div class="mt-4 p-3 rounded bg-gray-800/50 border border-gray-700 text-xs text-gray-400 space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span>Creado:</span>
|
||||
<span class="text-gray-300">{formatDate(location.created_at)}</span>
|
||||
</div>
|
||||
{#if location.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span>Actualizado:</span>
|
||||
<span class="text-gray-300">{formatDate(location.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isEdit && location}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(location.created_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-700 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={closeModal}
|
||||
class="px-4 py-2 text-sm font-medium text-gray-300 bg-transparent border border-gray-600 rounded-lg hover:bg-gray-800 hover:text-white transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-900/20"
|
||||
>
|
||||
{loading ? 'Guardando...' : (location ? 'Guardar Cambios' : 'Crear')}
|
||||
</button>
|
||||
{#if location.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(location.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,200 @@
|
||||
<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 { Textarea } from "$lib/components/ui/textarea"; // Asegúrate de tener este componente o usa <textarea class="...">
|
||||
|
||||
import {
|
||||
createPrevalidator,
|
||||
updatePrevalidator,
|
||||
type Prevalidator
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
prevalidator = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
prevalidator?: Prevalidator | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!prevalidator);
|
||||
const title = $derived(isEdit ? "Editar Prevalidador" : "Nuevo Prevalidador");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: '',
|
||||
customs_prevalidator: '',
|
||||
patent_prevalidator: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Función auxiliar para fechas
|
||||
function formatDate(dateString?: string) {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('es-MX', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (prevalidator) {
|
||||
formData = {
|
||||
code: prevalidator.code,
|
||||
description: prevalidator.description || '',
|
||||
customs_prevalidator: prevalidator.customs_prevalidator || '',
|
||||
patent_prevalidator: prevalidator.patent_prevalidator || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
description: '',
|
||||
customs_prevalidator: '',
|
||||
patent_prevalidator: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Validaciones
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
if (formData.code.length > 20) throw new Error('El código excede los 20 caracteres');
|
||||
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
description: formData.description.trim(),
|
||||
customs_prevalidator: formData.customs_prevalidator.trim(),
|
||||
patent_prevalidator: formData.patent_prevalidator.trim()
|
||||
};
|
||||
|
||||
if (isEdit && prevalidator) {
|
||||
await updatePrevalidator(prevalidator.id, dataToSend);
|
||||
} else {
|
||||
await createPrevalidator(dataToSend);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar el prevalidador';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej. PREVAL_01"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 20 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="customs" class="text-right">Aduana</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="customs"
|
||||
bind:value={formData.customs_prevalidator}
|
||||
placeholder="Ej. 240"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="patent" class="text-right">Patente</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="patent"
|
||||
bind:value={formData.patent_prevalidator}
|
||||
placeholder="Ej. 1234"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label for="description" class="text-right pt-2">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Descripción breve..."
|
||||
maxlength={50}
|
||||
disabled={loading}
|
||||
class="resize-none min-h-[80px]"
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 50 caracteres</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isEdit && prevalidator}
|
||||
<div class="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1 mt-2 border">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Creado:</span>
|
||||
<span>{formatDate(prevalidator.created_at)}</span>
|
||||
</div>
|
||||
{#if prevalidator.updated_at}
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Actualizado:</span>
|
||||
<span>{formatDate(prevalidator.updated_at)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user