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

@@ -151,19 +151,19 @@ class DodaContainerSeal(Base, TenantScopedMixin, TimestampMixin):
{"schema": "a76"},
)
# Primary key
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
# Foreign key and line info
container_id: Mapped[int] = mapped_column(Integer, nullable=False)
doda_id: Mapped[int] = mapped_column(Integer, nullable=False)
seal_line: Mapped[int] = mapped_column(Integer, nullable=False)
# Seal information
seal_value: Mapped[Optional[str]] = mapped_column(String(21))
# Relationships
container: Mapped["DodaContainer"] = relationship(
"DodaContainer", back_populates="seals_detail"
)

View File

@@ -31,7 +31,9 @@ class Prevalidator(Base, TenantScopedMixin, TimestampMixin):
# Prevalidator information
customs_prevalidator: Mapped[Optional[str]] = mapped_column(String(20))
patent_prevalidator: Mapped[Optional[str]] = mapped_column(String(20))
description: Mapped[Optional[str]] = mapped_column(String(50))
def __repr__(self):

View File

@@ -1,61 +1,157 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface DODA {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
export interface DodaContainerSeal {
id: number;
doda_sys_id: number;
seal_line: number;
seal_value?: string;
}
export interface DODACreate {
code: string;
description?: string;
export interface DodaContainerSealCreate {
seal_value?: string;
}
export interface DODAUpdate extends Partial<DODACreate> {}
export interface DODAListResponse {
items: DODA[];
total: number;
page: number;
page_size: number;
pages: number;
export interface DodaContainer {
id: number;
doda_sys_id: number;
container_line: number;
container_value?: string;
seals?: string;
seals_detail?: DodaContainerSeal[];
}
export async function getDODAs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<DODAListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/doda?${params.toString()}`);
return response.data;
export interface DodaContainerCreate {
container_value?: string;
seals?: string;
seals_detail?: DodaContainerSealCreate[];
}
export async function getDODA(id: number): Promise<DODA> {
const response = await api.get(`/a76/doda/${id}`);
return response.data;
export interface DodaAmericanPedimento {
id: number;
doda_sys_id: number;
american_pedimento_line: number;
american_pedimento_type?: string;
american_pedimento_value?: string;
}
export async function createDODA(data: DODACreate): Promise<DODA> {
const response = await api.post('/a76/doda', data);
return response.data;
export interface DodaAmericanPedimentoCreate {
american_pedimento_type?: string;
american_pedimento_value?: string;
}
export async function updateDODA(id: number, data: DODAUpdate): Promise<DODA> {
const response = await api.patch(`/a76/doda/${id}`, data);
return response.data;
export interface DodaPedimento {
id: number;
doda_sys_id: number;
pedimento_line: number;
authorization_patent?: string;
document?: string;
shipment?: string;
cove?: string;
umc?: string;
effective_amount_usd?: number;
difference_amount_usd?: number;
dta_niu?: string;
article_7?: boolean;
pedimento_sys_id?: number;
invoice_line?: number;
part_ii_line?: number;
pedimento_type?: string;
zero_packaging_validation?: boolean;
}
export async function deleteDODA(id: number): Promise<void> {
await api.delete(`/a76/doda/${id}`);
export interface DodaPedimentoCreate {
authorization_patent?: string;
document?: string;
shipment?: string;
effective_amount_usd?: number;
}
export interface Doda {
sys_id: number;
integration_number?: string;
doda_date?: number;
doda_time?: number;
dispatch_customs?: string;
customs_sections?: string;
patent?: string;
pedimentos?: string;
caat?: string;
transport_identification?: string;
fast_id?: string;
operation_type?: string;
status?: string;
containers?: DodaContainer[];
american_pedimentos?: DodaAmericanPedimento[];
pedimentos_detail?: DodaPedimento[];
tenant_id?: string;
created_at?: string;
updated_at?: string;
}
export interface DodaCreate {
integration_number?: string;
doda_date?: number;
doda_time?: number;
dispatch_customs?: string;
customs_sections?: string;
patent?: string;
caat?: string;
transport_identification?: string;
fast_id?: string;
}
export interface DodaUpdate extends Partial<DodaCreate> {}
export interface DodaListResponse {
items: Doda[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getDodas(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<DodaListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/v1/a76/doda?${params.toString()}`);
return response.data;
}
export async function getDoda(id: number): Promise<Doda> {
const response = await api.get(`/v1/a76/doda/${id}`);
return response.data;
}
export async function createDoda(data: DodaCreate): Promise<Doda> {
const response = await api.post('/v1/a76/doda', data);
return response.data;
}
export async function updateDoda(id: number, data: DodaUpdate): Promise<Doda> {
const response = await api.patch(`/v1/a76/doda/${id}`, data);
return response.data;
}
export async function deleteDoda(id: number): Promise<void> {
await api.delete(`/v1/a76/doda/${id}`);
}

View File

@@ -2,60 +2,81 @@ import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ElectronicNotice {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
id: number;
// Campos del Modelo Python
notice_number?: string;
year?: string;
patent?: string;
pedimento?: string;
file_sent?: string;
file_response?: string;
status?: string;
invoice?: string;
validation_acknowledgment?: string;
fea?: string;
certificate_number?: string;
// Mixins
tenant_id?: string;
created_at?: string;
updated_at?: string;
}
export interface ElectronicNoticeCreate {
code: string;
description?: string;
notice_number?: string;
year?: string;
patent?: string;
pedimento?: string;
file_sent?: string;
file_response?: string;
status?: string;
invoice?: string;
validation_acknowledgment?: string;
fea?: string;
certificate_number?: string;
}
export interface ElectronicNoticeUpdate extends Partial<ElectronicNoticeCreate> {}
export interface ElectronicNoticeListResponse {
items: ElectronicNotice[];
total: number;
page: number;
page_size: number;
pages: number;
items: ElectronicNotice[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getElectronicNotices(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ElectronicNoticeListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/electronic_notices?${params.toString()}`);
return response.data;
// Agregamos '/' al final
const response = await api.get(`/a76/electronic_notices/?${params.toString()}`);
return response.data;
}
export async function getElectronicNotice(id: number): Promise<ElectronicNotice> {
const response = await api.get(`/a76/electronic_notices/${id}`);
return response.data;
const response = await api.get(`/a76/electronic_notices/${id}`);
return response.data;
}
export async function createElectronicNotice(data: ElectronicNoticeCreate): Promise<ElectronicNotice> {
const response = await api.post('/a76/electronic_notices', data);
return response.data;
const response = await api.post('/a76/electronic_notices', data);
return response.data;
}
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate): Promise<ElectronicNotice> {
const response = await api.patch(`/a76/electronic_notices/${id}`, data);
return response.data;
const response = await api.patch(`/a76/electronic_notices/${id}`, data);
return response.data;
}
export async function deleteElectronicNotice(id: number): Promise<void> {
await api.delete(`/a76/electronic_notices/${id}`);
}
await api.delete(`/a76/electronic_notices/${id}`);
}

View File

@@ -2,60 +2,66 @@ import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Prevalidator {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
id: number;
code: string;
description?: string;
// Campos que faltaban según tu modelo Python:
customs_prevalidator?: string;
patent_prevalidator?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface PrevalidatorCreate {
code: string;
description?: string;
code: string;
description?: string;
customs_prevalidator?: string;
patent_prevalidator?: string;
}
export interface PrevalidatorUpdate extends Partial<PrevalidatorCreate> {}
export interface PrevalidatorListResponse {
items: Prevalidator[];
total: number;
page: number;
page_size: number;
pages: number;
items: Prevalidator[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getPrevalidators(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<PrevalidatorListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/prevalidators?${params.toString()}`);
return response.data;
const response = await api.get(`/a76/prevalidators/?${params.toString()}`);
return response.data;
}
export async function getPrevalidator(id: number): Promise<Prevalidator> {
const response = await api.get(`/a76/prevalidators/${id}`);
return response.data;
const response = await api.get(`/a76/prevalidators/${id}`);
return response.data;
}
export async function createPrevalidator(data: PrevalidatorCreate): Promise<Prevalidator> {
const response = await api.post('/a76/prevalidators', data);
return response.data;
const response = await api.post('/a76/prevalidators', data);
return response.data;
}
export async function updatePrevalidator(id: number, data: PrevalidatorUpdate): Promise<Prevalidator> {
const response = await api.patch(`/a76/prevalidators/${id}`, data);
return response.data;
const response = await api.patch(`/a76/prevalidators/${id}`, data);
return response.data;
}
export async function deletePrevalidator(id: number): Promise<void> {
await api.delete(`/a76/prevalidators/${id}`);
}
await api.delete(`/a76/prevalidators/${id}`);
}

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>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
@@ -81,4 +82,9 @@
totalItems={data.dodas?.total || 0}
/>
</div>
<CreateDialog
bind:open={dialogOpen}
on:success={handleSuccess}
/>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/electronic-notices/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
@@ -81,4 +82,10 @@
totalItems={data.notices?.total || 0}
/>
</div>
<CreateDialog
bind:open={dialogOpen}
on:success={handleSuccess}
/>
</div>

View File

@@ -7,7 +7,7 @@
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import CreateEditDialog from '$lib/components/dashboard/classes/create-edit-dialog.svelte';
let { data } = $props();
let dialogOpen = $state(false);
@@ -42,6 +42,7 @@
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">

View File

@@ -1,223 +1,192 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Card from '$lib/components/ui/card';
import * as Alert from '$lib/components/ui/alert';
import { Plus, Search } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
import type { ExchangeRate } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { companyStore } from '$lib/stores/company.svelte';
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Card from '$lib/components/ui/card';
import * as Alert from '$lib/components/ui/alert';
import { Plus, Search } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
import type { ExchangeRate } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import { companyStore } from '$lib/stores/company.svelte';
let { data } = $props();
let { data } = $props();
let allItems = $state<ExchangeRate[]>(data.exchangeRates?.items || []);
let loading = $state(false);
let error = $state<string | null>(data.error || null);
let currentPage = $state(1);
const pageSize = 50;
let allItems = $state<ExchangeRate[]>(data.exchangeRates?.items || []);
let loading = $state(false);
let error = $state<string | null>(data.error || null);
let currentPage = $state(1);
const pageSize = 50;
let dialogOpen = $state(false);
let editingItem = $state<ExchangeRate | null>(null);
let dialogOpen = $state(false);
let editingItem = $state<ExchangeRate | null>(null);
// Filters
let dateFilter = $state('');
let localCurrencyFilter = $state('');
let foreignCurrencyFilter = $state('');
// Filters
let dateFilter = $state('');
let localCurrencyFilter = $state('');
let foreignCurrencyFilter = $state('');
let hasMore = $derived(allItems.length >= currentPage * pageSize);
let hasMore = $derived(allItems.length >= currentPage * pageSize);
onMount(() => {
// Sync access_token from cookies to localStorage
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'access_token') {
localStorage.setItem('access_token', value);
break;
}
}
onMount(() => {
// Sync access_token from cookies to localStorage
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'access_token') {
localStorage.setItem('access_token', value);
break;
}
}
// Carga inicial reactiva al store
if (companyStore.activeCompany) {
loadInitialData();
}
});
// Esperar a que el companyStore esté inicializado antes de cargar datos
const checkAndLoad = () => {
if (companyStore.activeCompany) {
loadInitialData();
} else {
// Si no hay compañía, esperar un poco y reintentar
setTimeout(checkAndLoad, 100);
}
};
checkAndLoad();
// Efecto reactivo: si cambia la compañía, recargar
$effect(() => {
if (companyStore.activeCompany?.id) {
loadInitialData();
}
});
// Listen for company changes
const handleCompanyChange = () => {
loadInitialData();
};
window.addEventListener('companyChanged', handleCompanyChange);
async function loadInitialData() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
return () => {
window.removeEventListener('companyChanged', handleCompanyChange);
};
});
allItems = [];
currentPage = 1;
await loadExchangeRates(1);
}
async function loadInitialData() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
return;
}
async function loadExchangeRates(page: number) {
const companyId = companyStore.activeCompany?.id;
if (!companyId || loading) return;
allItems = [];
currentPage = 1;
await loadExchangeRates(1);
}
loading = true;
error = null;
async function loadExchangeRates(page: number) {
const companyId = companyStore.activeCompany?.id;
if (!companyId || loading) {
return;
}
try {
const filters: any = {
page,
page_size: pageSize
};
loading = true;
error = null;
if (dateFilter) filters.date = dateFilter;
if (localCurrencyFilter) filters.local_currency = localCurrencyFilter;
if (foreignCurrencyFilter) filters.foreign_currency = foreignCurrencyFilter;
try {
const filters: any = {
page,
page_size: pageSize
};
const response = await getExchangeRates(companyId, filters);
if (dateFilter) filters.date = dateFilter;
if (localCurrencyFilter) filters.local_currency = localCurrencyFilter;
if (foreignCurrencyFilter) filters.foreign_currency = foreignCurrencyFilter;
if (page === 1) {
allItems = response.items;
} else {
allItems = [...allItems, ...response.items];
}
currentPage = page;
} catch (err: any) {
error = err.message || 'Error al cargar los tipos de cambio';
} finally {
loading = false;
}
}
const response = await getExchangeRates(companyId, filters);
function loadMore() {
if (!loading && hasMore) {
loadExchangeRates(currentPage + 1);
}
}
if (page === 1) {
allItems = response.items;
} else {
allItems = [...allItems, ...response.items];
}
function handleSearch() {
loadInitialData();
}
currentPage = page;
} catch (err: any) {
error = err.message || 'Error al cargar los tipos de cambio';
} finally {
loading = false;
}
}
function handleCreate() {
editingItem = null;
dialogOpen = true;
}
function loadMore() {
if (!loading && hasMore) {
loadExchangeRates(currentPage + 1);
}
}
// Esta función se pasa a las columnas
function handleEdit(item: ExchangeRate) {
editingItem = item;
dialogOpen = true;
}
function handleSearch() {
loadInitialData();
}
function handleSuccess(item?: ExchangeRate) {
loadInitialData();
}
function handleCreate() {
editingItem = null;
dialogOpen = true;
}
function handleEdit(item: ExchangeRate) {
editingItem = item;
dialogOpen = true;
}
function handleSuccess(item?: ExchangeRate) {
// Reload data after create/edit/delete
loadInitialData();
}
const columns = createColumns(handleSuccess);
const columns = createColumns(handleEdit); // Pasamos handleEdit en lugar de handleSuccess
</script>
<svelte:head>
<title>Tipos de Cambio - Anexo 76</title>
<title>Tipos de Cambio - Anexo 76</title>
</svelte:head>
<div class="space-y-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold tracking-tight">Tipos de Cambio</h1>
<p class="text-muted-foreground">Gestiona los tipos de cambio del sistema</p>
</div>
<Button onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Crear Tipo de Cambio
</Button>
</div>
<div class="space-y-6 p-4">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold tracking-tight">Tipos de Cambio</h1>
<p class="text-muted-foreground">Gestiona los tipos de cambio del sistema</p>
</div>
<Button href="/dashboard/general_catalogs/exchange-rate/new">
<Plus class="mr-2 h-4 w-4" />
Crear Tipo de Cambio
</Button>
</div>
<Card.Root>
<Card.Header>
<Card.Title>Filtros</Card.Title>
</Card.Header>
<Card.Content>
<div class="grid gap-4 md:grid-cols-4">
<div class="space-y-2">
<Label for="date-filter">Fecha</Label>
<Input
id="date-filter"
type="date"
bind:value={dateFilter}
placeholder="Buscar por fecha..."
/>
</div>
<div class="space-y-2">
<Label for="local-currency-filter">Moneda Local</Label>
<Input
id="local-currency-filter"
bind:value={localCurrencyFilter}
placeholder="MXN"
/>
</div>
<div class="space-y-2">
<Label for="foreign-currency-filter">Moneda Extranjera</Label>
<Input
id="foreign-currency-filter"
bind:value={foreignCurrencyFilter}
placeholder="USD"
/>
</div>
<div class="flex items-end">
<Button onclick={handleSearch} class="w-full">
<Search class="mr-2 h-4 w-4" />
Buscar
</Button>
</div>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title>Filtros</Card.Title>
</Card.Header>
<Card.Content>
<div class="grid gap-4 md:grid-cols-4 items-end">
<div class="space-y-2">
<Label for="date-filter">Fecha</Label>
<Input id="date-filter" type="date" bind:value={dateFilter} />
</div>
<div class="space-y-2">
<Label for="local-currency-filter">Moneda Local</Label>
<Input id="local-currency-filter" placeholder="MXN" bind:value={localCurrencyFilter} />
</div>
<div class="space-y-2">
<Label for="foreign-currency-filter">Moneda Extranjera</Label>
<Input id="foreign-currency-filter" placeholder="USD" bind:value={foreignCurrencyFilter} />
</div>
<Button onclick={handleSearch} class="w-full" variant="secondary">
<Search class="mr-2 h-4 w-4" />
Buscar
</Button>
</div>
</Card.Content>
</Card.Root>
{#if error}
<Alert.Root variant="destructive">
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
<CreateEditDialog
bind:open={dialogOpen}
item={editingItem}
onSuccess={handleSuccess}
/>
</div>
{#if error}
<Alert.Root variant="destructive">
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
<div class="rounded-md border bg-card">
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
loadMore={loadMore}
/>
</div>
<CreateEditDialog
bind:open={dialogOpen}
item={editingItem}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Card from "$lib/components/ui/card";
import { ArrowLeft } from 'lucide-svelte';
import { createExchangeRate } from "$lib/api/dashboard/a76/general_catalogs/exchange-rate";
import { companyStore } from "$lib/stores/company.svelte";
let loading = $state(false);
let error = $state<string | null>(null);
let formData = $state({
date: new Date().toISOString().split('T')[0], // Fecha de hoy
value: null as number | null,
local_currency: 'MXN',
foreign_currency: 'USD'
});
async function handleSubmit() {
loading = true;
error = null;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) throw new Error('No hay una compañía seleccionada');
if (!formData.date) throw new Error('La fecha es requerida');
if (formData.value === null) throw new Error('El valor es requerido');
const dataToSend = {
date: formData.date,
value: Number(formData.value),
local_currency: formData.local_currency?.trim().toUpperCase() || null,
foreign_currency: formData.foreign_currency?.trim().toUpperCase() || null
};
await createExchangeRate(dataToSend, companyId);
// Éxito: Regresar a la lista
goBack();
} catch (e) {
error = e instanceof Error ? e.message : 'Error al guardar';
} finally {
loading = false;
}
}
function goBack() {
// Navegar un nivel arriba (a la lista)
goto('/dashboard/general_catalogs/exchange-rate');
}
</script>
<svelte:head>
<title>Nuevo Tipo de Cambio - Anexo 76</title>
</svelte:head>
<div class="max-w-2xl mx-auto space-y-6 p-4">
<div class="flex items-center gap-4">
<Button variant="ghost" size="icon" onclick={goBack}>
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-2xl font-bold tracking-tight">Nuevo Tipo de Cambio</h1>
<p class="text-muted-foreground">Registra un nuevo tipo de cambio en el sistema</p>
</div>
</div>
<Card.Root>
<Card.Content class="pt-6">
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6">
{#if error}
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive border border-destructive/20">
{error}
</div>
{/if}
<div class="grid gap-6">
<div class="grid md:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="date">Fecha <span class="text-destructive">*</span></Label>
<Input id="date" type="date" bind:value={formData.date} disabled={loading} required />
</div>
<div class="space-y-2">
<Label for="value">Valor <span class="text-destructive">*</span></Label>
<Input
id="value"
type="number"
step="0.000001"
bind:value={formData.value}
disabled={loading}
required
placeholder="0.000000"
/>
<p class="text-xs text-muted-foreground">Hasta 6 decimales de precisión.</p>
</div>
</div>
<div class="grid md:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="local_currency">Moneda Local</Label>
<Input
id="local_currency"
bind:value={formData.local_currency}
maxlength={3}
disabled={loading}
placeholder="MXN"
class="uppercase"
/>
</div>
<div class="space-y-2">
<Label for="foreign_currency">Moneda Extranjera</Label>
<Input
id="foreign_currency"
bind:value={formData.foreign_currency}
maxlength={3}
disabled={loading}
placeholder="USD"
class="uppercase"
/>
</div>
</div>
</div>
<div class="flex justify-end gap-3 pt-4">
<Button type="button" variant="outline" onclick={goBack} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : 'Guardar Tipo de Cambio'}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import CreateDialog from '$lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
@@ -81,4 +82,10 @@
totalItems={data.prevalidators?.total || 0}
/>
</div>
<CreateDialog
open={dialogOpen}
on:close={() => dialogOpen = false}
on:success={handleSuccess}
/>
</div>