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