Se termino de agregar los formularios modales a los catalogos que faltaba

This commit is contained in:
2025-12-22 11:13:35 -06:00
parent 153fca9ad7
commit 6a9f69e39e
18 changed files with 1570 additions and 642 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">
&times;
</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>

View File

@@ -23,7 +23,6 @@
const isEdit = $derived(!!item);
const title = $derived(isEdit ? "Editar INPC" : "Nuevo INPC");
// Estado del formulario
let formData = $state({
year: '',
month: '',

View File

@@ -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">
&times;
</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>

View File

@@ -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>