fix: resolver conflicto de merge en signatures.ts

Restaura la construcción correcta de URLSearchParams en getSignatures,
eliminando el spread de filters inválido y el cierre de objeto roto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 17:42:35 -05:00
45 changed files with 1218 additions and 579 deletions

View File

@@ -196,6 +196,7 @@
</Dialog.Description>
</Dialog.Header>
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
<div class="grid gap-6 py-4">
<div class="space-y-4">
<h4 class="text-sm leading-none font-medium text-muted-foreground">Identificación</h4>
@@ -404,6 +405,7 @@
</div>
</div>
</div>
</form>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)} disabled={loading}>Cancelar</Button>

View File

@@ -183,7 +183,7 @@
</Dialog.Description>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-6">
<form onsubmit={handleSubmit} autocomplete="off" class="space-y-6">
{#if error}
<div
class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive"

View File

@@ -175,6 +175,7 @@
<Dialog.Content
class="fixed top-[50%] left-[50%] z-[10000] w-full max-w-[480px] translate-x-[-50%] translate-y-[-50%] overflow-hidden border-0 bg-transparent p-0 shadow-2xl sm:rounded-xl"
onInteractOutside={(e) => e.preventDefault()}
>
<div
class="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-background"

View File

@@ -13,6 +13,11 @@ export function createColumns(
header: 'Clasificación',
cell: ({ row }) => row.original.classification || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',

View File

@@ -978,6 +978,7 @@
<Dialog.Content
class="flex h-[90dvh] max-h-[90dvh] w-[calc(100vw-2rem)] max-w-5xl flex-col gap-0 overflow-hidden border bg-background p-0 shadow-lg sm:max-w-5xl"
showCloseButton={true}
onInteractOutside={(e) => e.preventDefault()}
>
<Tabs.Root bind:value={activeTab} class="flex min-h-0 w-full flex-1 flex-col gap-0">
<Dialog.Header class="shrink-0 space-y-2 border-b px-4 pb-3 pt-3 pr-12 text-left sm:text-left">

View File

@@ -117,7 +117,7 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<Dialog.Content class="sm:max-w-[600px] max-h-[90vh] overflow-y-auto" onInteractOutside={(e) => e.preventDefault()}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>

View File

@@ -3,15 +3,19 @@
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
// 👇 Verifica la ruta de tu archivo TS
import { Search } from 'lucide-svelte';
import {
createMultiCurrencyType,
updateMultiCurrencyType,
type MultiCurrencyType
} from '$lib/api/dashboard/a76/general_catalogs/multi-currency-types';
import { companyStore } from '$lib/stores/company.svelte';
import { obtenerAtajosFormularioMonedas } from '$lib/config/shortcuts/dashboard/general_catalogs/multi_currency_types/edit';
import { m } from '$lib/i18n/messages';
import CurrencySelectorDialog from '$lib/components/dashboard/goods/modales/currency-selector-dialog.svelte';
import CountrySelectorDialog from '$lib/components/dashboard/goods/modales/country-selector-dialog.svelte';
import type { CurrencyType } from '$lib/api/dashboard/reference_data/currency_types';
import type { Country } from '$lib/api/dashboard/reference_data/countries';
import { toast } from 'svelte-sonner';
let {
open = $bindable(false),
@@ -23,45 +27,36 @@
onSuccess?: () => void;
} = $props();
// Atajos
const isEdit = $derived(!!item);
const title = $derived(
isEdit ? m.multi_currency_edit_title() : m.multi_currency_new_title()
);
const title = $derived(isEdit ? m.multi_currency_edit_title() : m.multi_currency_new_title());
// Estado del formulario
let formData = $state({
currency_type_code: '',
country_key: '',
conversion_factor: null as number | null,
date_str: '' // Usamos un string temporal para el input type="date"
date_str: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
let showCurrencyDialog = $state(false);
let showCountryDialog = $state(false);
// Cargar datos al abrir
$effect(() => {
if (open) {
if (item) {
// Truco: Convertir Entero (20251231) -> String ("2025-12-31")
const s = item.publication_date?.toString();
let dateFormatted = '';
if (item.publication_date) {
const s = item.publication_date.toString();
if (s.length === 8) {
dateFormatted = `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
if (s?.length === 8) {
dateFormatted = `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
formData = {
currency_type_code: item.currency_type_code,
country_key: item.country_key || '',
conversion_factor: item.conversion_factor,
date_str: dateFormatted
};
} else {
// Default: Fecha de hoy
} else {
formData = {
currency_type_code: '',
country_key: '',
@@ -73,6 +68,16 @@
}
});
function handleCurrencySelect(currency: CurrencyType) {
formData.currency_type_code = currency.code;
toast.success(`${currency.code}${currency.currency_name}`);
}
function handleCountrySelect(country: Country) {
formData.country_key = country.m3_key;
toast.success(`${country.m3_key}${country.description_es || country.description_en || ''}`);
}
async function handleSubmit() {
loading = true;
error = null;
@@ -80,33 +85,23 @@
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) throw new Error(m.exchange_rate_error_no_company());
// Validaciones
if (!formData.currency_type_code.trim()) throw new Error(m.multi_currency_error_currency_required());
if (!formData.date_str) throw new Error(m.multi_currency_error_date_required());
// Truco: Convertir String ("2025-12-31") -> Entero (20251231)
// Quitamos los guiones y parseamos a int
const dateInt = parseInt(formData.date_str.replaceAll('-', ''), 10);
// Preparar datos
const dataToSend = {
currency_type_code: formData.currency_type_code.trim().toUpperCase(),
country_key: formData.country_key.trim().toUpperCase() || null,
conversion_factor: formData.conversion_factor ? Number(formData.conversion_factor) : null,
publication_date: dateInt // Mandamos el INT que espera Python
publication_date: dateInt
};
let response;
if (isEdit && item) {
response = await updateMultiCurrencyType(item.id, dataToSend, companyId);
} else {
response = await createMultiCurrencyType(dataToSend, companyId);
}
const response = isEdit && item
? await updateMultiCurrencyType(item.id, dataToSend, companyId)
: await createMultiCurrencyType(dataToSend, companyId);
if (response?.error) {
throw new Error(response.error);
}
if (response?.error) throw new Error(response.error);
open = false;
if (onSuccess) onSuccess();
@@ -119,16 +114,16 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[500px]">
<Dialog.Content
class="sm:max-w-[500px]"
onInteractOutside={(e) => e.preventDefault()}
>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}
class="space-y-4 py-4"
>
{#if error}
@@ -138,41 +133,77 @@
{/if}
<div class="grid gap-4">
<!-- Código Moneda -->
<div class="grid grid-cols-4 items-center gap-4">
<Label for="currency_code" class="text-right"
>{m.multi_currency_currency_label()} <span class="text-destructive">*</span></Label
>
<div class="col-span-3">
<Label for="currency_code" class="text-right">
{m.multi_currency_currency_label()} <span class="text-destructive">*</span>
</Label>
<div class="col-span-3 flex gap-2">
<Input
id="currency_code"
bind:value={formData.currency_type_code}
placeholder="{m.exchange_rate_example_suffix()} USD"
oninput={(e) => {
formData.currency_type_code = e.currentTarget.value
.toUpperCase()
.replace(/[^A-Z]/g, '')
.slice(0, 3);
e.currentTarget.value = formData.currency_type_code;
}}
placeholder="Ej. USD"
maxlength={3}
disabled={loading}
required
class="flex-1 font-mono"
/>
<p class="text-[10px] text-muted-foreground mt-1">{m.multi_currency_currency_help()}</p>
<Button
type="button"
variant="outline"
size="icon"
class="h-10 w-10 shrink-0"
onclick={() => (showCurrencyDialog = true)}
disabled={loading}
>
<Search class="h-4 w-4" />
</Button>
</div>
</div>
<!-- País -->
<div class="grid grid-cols-4 items-center gap-4">
<Label for="country_key" class="text-right">{m.multi_currency_country_label()}</Label>
<div class="col-span-3">
<div class="col-span-3 flex gap-2">
<Input
id="country_key"
bind:value={formData.country_key}
placeholder="{m.exchange_rate_example_suffix()} MEX"
oninput={(e) => {
formData.country_key = e.currentTarget.value
.toUpperCase()
.replace(/[^A-Z]/g, '')
.slice(0, 3);
e.currentTarget.value = formData.country_key;
}}
placeholder="Ej. MEX"
maxlength={3}
disabled={loading}
class="flex-1 font-mono"
/>
<p class="text-[10px] text-muted-foreground mt-1">{m.multi_currency_country_help()}</p>
<Button
type="button"
variant="outline"
size="icon"
class="h-10 w-10 shrink-0"
onclick={() => (showCountryDialog = true)}
disabled={loading}
>
<Search class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Fecha Publicación -->
<div class="grid grid-cols-4 items-center gap-4">
<Label for="pub_date" class="text-right"
>{m.multi_currency_date_label()} <span class="text-destructive">*</span></Label
>
<Label for="pub_date" class="text-right">
{m.multi_currency_date_label()} <span class="text-destructive">*</span>
</Label>
<div class="col-span-3">
<Input
id="pub_date"
@@ -181,10 +212,11 @@
disabled={loading}
required
/>
<p class="text-[10px] text-muted-foreground mt-1">{m.multi_currency_date_help()}</p>
<p class="mt-1 text-[10px] text-muted-foreground">{m.multi_currency_date_help()}</p>
</div>
</div>
<!-- Factor de Conversión -->
<div class="grid grid-cols-4 items-center gap-4">
<Label for="factor" class="text-right">{m.multi_currency_factor_label()}</Label>
<div class="col-span-3">
@@ -211,3 +243,6 @@
</form>
</Dialog.Content>
</Dialog.Root>
<CurrencySelectorDialog bind:open={showCurrencyDialog} onSelect={handleCurrencySelect} />
<CountrySelectorDialog bind:open={showCountryDialog} onSelect={handleCountrySelect} />

View File

@@ -156,15 +156,18 @@
onclick={(e) => {
if ((e.target as HTMLElement).closest('input[type="checkbox"]')) return;
const rowId = getRowIdValue(row.original);
if (rowId !== null) {
const isSelected = selectedIds.includes(rowId);
if (isSelected) {
onSelectedIdsChange?.(selectedIds.filter((id) => id !== rowId));
} else {
// El id clickeado va primero para que el cuadro informativo lo muestre
onSelectedIdsChange?.([rowId, ...selectedIds.filter((id) => id !== rowId)]);
}
if (rowId === null) return;
// Click sobre la fila activa (la del frente) la deselecciona.
// Click sobre cualquier otra fila la trae al frente (mostrar su info).
if (selectedIds[0] === rowId) {
onSelectedIdsChange?.(selectedIds.filter((id) => id !== rowId));
return;
}
if (selectedIds.includes(rowId)) {
onSelectedIdsChange?.([rowId, ...selectedIds.filter((id) => id !== rowId)]);
return;
}
onSelectedIdsChange?.([rowId, ...selectedIds]);
}}
ondblclick={() => onRowDoubleClick?.(row.original)}
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"

View File

@@ -117,15 +117,23 @@
if (fraction) {
// Update
const updateData: HistoricalFractionUpdate = baseData;
await updateHistoricalFraction(companyId, fraction.id, updateData);
const updateResponse = await updateHistoricalFraction(companyId, fraction.id, updateData);
if (updateResponse.error || !updateResponse.data) {
toast.error(updateResponse.error || 'Error al actualizar la fracción');
return;
}
toast.success('Fracción actualizada correctamente');
} else {
// Create
const createData: HistoricalFractionCreate = {
...baseData,
historical_fraction: historicalFractionCode // Required in create
historical_fraction: historicalFractionCode
};
await createHistoricalFraction(companyId, createData);
const createResponse = await createHistoricalFraction(companyId, createData);
if (createResponse.error || !createResponse.data) {
toast.error(createResponse.error || 'Error al crear la fracción');
return;
}
toast.success('Fracción creada correctamente');
}
onSuccess();
@@ -140,7 +148,7 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[700px]">
<Dialog.Content class="sm:max-w-[700px]" onInteractOutside={(e) => e.preventDefault()}>
<Dialog.Header>
<Dialog.Title>{fraction ? 'Editar' : 'Crear'} Fracción Histórica</Dialog.Title>
<Dialog.Description>

View File

@@ -12,6 +12,7 @@
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import HistoricalFractionDialog from './HistoricalFractionDialog.svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { currentUser } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import {
@@ -55,6 +56,8 @@ let scrollContainer = $state<HTMLDivElement | null>(null);
let dialogOpen = $state(false);
let editingFraction = $state<HistoricalFraction | null>(null);
let deletingFractionId = $state<number | null>(null);
let showDeleteConfirm = $state(false);
let fractionToDelete = $state<HistoricalFraction | null>(null);
async function loadFractions(reset = false) {
const companyId = companyStore.activeCompany?.id;
@@ -136,19 +139,27 @@ let scrollContainer = $state<HTMLDivElement | null>(null);
dialogOpen = true;
}
async function handleDelete(fraction: HistoricalFraction) {
function confirmDelete(fraction: HistoricalFraction) {
if (!canDelete) {
toast.error('No tienes permiso para eliminar fracciones históricas');
return;
}
fractionToDelete = fraction;
showDeleteConfirm = true;
}
async function handleDelete() {
if (!canDelete || !fractionToDelete) return;
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
if (!confirm(`¿Estás seguro de eliminar la fracción ${fraction.historical_fraction}?`)) return;
try {
deletingFractionId = fraction.id;
await deleteHistoricalFraction(companyId, fraction.id);
deletingFractionId = fractionToDelete.id;
const response = await deleteHistoricalFraction(companyId, fractionToDelete.id);
if (response.error) {
toast.error(response.error || 'Error al eliminar la fracción');
return;
}
toast.success('Fracción eliminada correctamente');
loadFractions(true);
} catch (error) {
@@ -156,6 +167,8 @@ let scrollContainer = $state<HTMLDivElement | null>(null);
toast.error('Error al eliminar la fracción');
} finally {
deletingFractionId = null;
showDeleteConfirm = false;
fractionToDelete = null;
}
}
@@ -266,7 +279,7 @@ let scrollContainer = $state<HTMLDivElement | null>(null);
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Row class="catalog-table-row cursor-pointer" ondblclick={() => canEdit && handleEdit(fraction)}>
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
@@ -288,7 +301,7 @@ let scrollContainer = $state<HTMLDivElement | null>(null);
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
onclick={() => confirmDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
@@ -330,3 +343,26 @@ let scrollContainer = $state<HTMLDivElement | null>(null);
/>
{/if}
</div>
<AlertDialog.Root bind:open={showDeleteConfirm}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
<AlertDialog.Description>
Esta acción no se puede deshacer. Se eliminará permanentemente la fracción <strong>{fractionToDelete?.historical_fraction}</strong>.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancelar</AlertDialog.Cancel>
<AlertDialog.Action
class="bg-destructive text-white hover:bg-destructive/90"
onclick={handleDelete}
>
{#if deletingFractionId !== null}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if}
Eliminar
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>

View File

@@ -84,9 +84,6 @@
return [...new Set(expanded)];
}
$inspect('HELP_DEBUG_PATH', currentPath);
$inspect('HELP_DEBUG_KEYWORDS', getKeywords(currentPath));
// Filtrar artículos contextuales basados en la ruta actual o coincidencias inteligentes
const contextualArticles = $derived(
articles.filter((a) => {

View File

@@ -5,6 +5,7 @@
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { authStore, userHasPermission } from '$lib/auth';
import { page } from '$app/state';
let {
items
@@ -23,6 +24,11 @@
}[];
} = $props();
function isUrlActive(url: string): boolean {
const pathname = page.url.pathname;
return pathname === url || pathname.startsWith(url + '/');
}
// Filtrar items según permisos (si el item tiene la propiedad 'permission')
const filteredItems = $derived(
items
@@ -195,7 +201,10 @@
</Sidebar.MenuItem>
{:else}
<!-- Sidebar Expandido: Collapsible original -->
<Collapsible.Root open={item.isActive} class="group/collapsible">
<Collapsible.Root
open={item.isActive || item.items?.some((sub) => isUrlActive(sub.url))}
class="group/collapsible"
>
{#snippet child({ props })}
<Sidebar.MenuItem {...props}>
<Collapsible.Trigger>
@@ -215,7 +224,7 @@
<Sidebar.MenuSub>
{#each item.items as subItem (subItem.title)}
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton>
<Sidebar.MenuSubButton isActive={isUrlActive(subItem.url)}>
{#snippet child({ props })}
<a href={subItem.url} {...props}>
<span>{subItem.title}</span>