fix/doble-click-parte

This commit is contained in:
2026-04-21 11:23:35 -06:00
parent 0a4cfb554b
commit 9783f487f3
10 changed files with 140 additions and 60 deletions

View File

@@ -14,6 +14,7 @@
selectedIds?: number[];
onSelectedIdsChange?: (ids: number[]) => void;
onRowClick?: (row: TData) => void;
onRowDoubleClick?: (row: TData) => void;
};
let {
@@ -26,6 +27,7 @@
selectedIds = [],
onSelectedIdsChange,
onRowClick,
onRowDoubleClick,
}: InfiniteDataTableProps<TData, TValue> = $props();
const table = createSvelteTable({

View File

@@ -12,6 +12,7 @@
loadMore: () => void;
selectedId?: number | null;
onRowClick?: (row: TData) => void;
onRowDoubleClick?: (row: TData) => void;
};
let {
@@ -21,7 +22,8 @@
hasMore,
loadMore,
selectedId = null,
onRowClick
onRowClick,
onRowDoubleClick
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
@@ -94,6 +96,7 @@
{#each table.getRowModel().rows as row (row.id)}
<Table.Row inTabOrder={false} data-state={row.getIsSelected() && 'selected'}
onclick={() => onRowClick?.(row.original)}
ondblclick={() => onRowDoubleClick?.(row.original)}
class="cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
>
{#each row.getVisibleCells() as cell (cell.id)}

View File

@@ -16,15 +16,34 @@ export function createColumns(): ColumnDef<A76Class>[] {
},
cell: ({ row }) => {
const isSelected = row.getIsSelected();
const checkboxSnippet = createRawSnippet<[{ selected: boolean }]>((getProps) => {
const { selected } = getProps();
const checkboxSnippet = createRawSnippet<[
{ selected: boolean; onchange: (e: Event) => void }
]>((getProps) => {
const { selected, onchange } = getProps();
return {
render: () => `<div class="flex items-center justify-center">
<input type="checkbox" tabindex="-1" class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" ${selected ? 'checked' : ''} />
</div>`
</div>`,
setup: (node) => {
const input = node.querySelector('input') as HTMLInputElement | null;
input?.addEventListener('change', onchange);
}
};
});
return renderSnippet(checkboxSnippet, { selected: isSelected });
return renderSnippet(checkboxSnippet, {
selected: isSelected,
onchange: (e: Event) => {
e.stopPropagation();
const rowId = row.original.id;
if (typeof rowId === 'number') {
(
row.table.options.meta as
| { toggleSelectedId?: (id: number, checked?: boolean) => void }
| undefined
)?.toggleSelectedId?.(rowId, (e.target as HTMLInputElement).checked);
}
}
});
},
size: 40,
enableSorting: false,

View File

@@ -10,8 +10,9 @@
columns: ColumnDef<TData, TValue>[];
data: TData[];
loading: boolean;
selectedId?: number | null;
onRowClick?: (row: TData) => void;
selectedIds?: number[];
onSelectedIdsChange?: (selectedIds: number[]) => void;
onRowDoubleClick?: (row: TData) => void;
sorting?: import("@tanstack/table-core").SortingState;
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
};
@@ -20,26 +21,50 @@
data,
columns,
loading,
selectedId = null,
onRowClick,
selectedIds = [],
onSelectedIdsChange,
onRowDoubleClick,
sorting = [],
onSortingChange
}: DataTableProps<TData, TValue> = $props();
function getRowIdValue(row: TData): number | null {
const candidate = (row as { id?: unknown })?.id;
return typeof candidate === 'number' ? candidate : null;
}
function toggleSelectedId(rowId: number, forceSelected?: boolean) {
const isSelected = selectedIds.includes(rowId);
const shouldSelect = forceSelected ?? !isSelected;
if (shouldSelect && !isSelected) {
onSelectedIdsChange?.([...selectedIds, rowId]);
return;
}
if (!shouldSelect && isSelected) {
onSelectedIdsChange?.(selectedIds.filter((id) => id !== rowId));
}
}
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row: any) => row.id?.toString(),
state: {
get rowSelection() {
return selectedId ? { [selectedId]: true } : {};
return Object.fromEntries(selectedIds.map((id) => [id.toString(), true]));
},
get sorting() {
return sorting;
}
},
meta: {
toggleSelectedId
},
onSortingChange: (updater) => {
if (onSortingChange) {
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
@@ -48,7 +73,7 @@
},
manualSorting: true,
enableRowSelection: true,
enableMultiRowSelection: false
enableMultiRowSelection: true
});
</script>
@@ -129,10 +154,12 @@
<Table.Row
data-state={row.getIsSelected() && "selected"}
onclick={() => {
if (onRowClick) {
onRowClick(row.original);
const rowId = getRowIdValue(row.original);
if (rowId !== null) {
toggleSelectedId(rowId);
}
}}
ondblclick={() => onRowDoubleClick?.(row.original)}
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
>
{#each row.getVisibleCells() as cell (cell.id)}

View File

@@ -140,10 +140,18 @@
/>
</div>
<div
class="flex-1 overflow-y-auto border rounded-md min-h-[300px]"
onscroll={handleScroll}
>
<div class="flex-1 flex flex-col overflow-hidden border rounded-md min-h-[300px]">
<!-- Header fuera del contenedor scrollable para evitar desplazamiento en hit-testing -->
<table class="w-full text-sm shrink-0">
<thead class="bg-muted/50">
<tr class="text-left border-b">
<th class="p-3 font-medium text-muted-foreground w-[100px]">Clave</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
<th class="p-3 font-medium text-muted-foreground w-[80px]">UM</th>
</tr>
</thead>
</table>
<div class="flex-1 overflow-y-auto" onscroll={handleScroll}>
{#if loading}
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
<Loader2 class="h-8 w-8 animate-spin text-primary" />
@@ -155,13 +163,6 @@
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0 backdrop-blur-sm">
<tr class="text-left border-b">
<th class="p-3 font-medium text-muted-foreground w-[100px]">Clave</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
<th class="p-3 font-medium text-muted-foreground w-[80px]">UM</th>
</tr>
</thead>
<tbody>
{#each items as item}
<tr
@@ -204,6 +205,7 @@
</div>
{/if}
{/if}
</div>
</div>
<Dialog.Footer>

View File

@@ -18,6 +18,7 @@
rowSelection?: import("@tanstack/table-core").RowSelectionState;
onRowSelectionChange?: (rowSelection: import("@tanstack/table-core").RowSelectionState) => void;
onRowClick?: (row: TData) => void;
onRowDoubleClick?: (row: TData) => void;
};
let {
@@ -30,7 +31,8 @@
onSortingChange,
rowSelection = {},
onRowSelectionChange,
onRowClick
onRowClick,
onRowDoubleClick
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
@@ -170,7 +172,8 @@
<Table.Row
data-state={row.getIsSelected() && "selected"}
onclick={() => onRowClick?.(row.original)}
class={onRowClick ? "cursor-pointer" : ""}
ondblclick={() => onRowDoubleClick?.(row.original)}
class={(onRowClick || onRowDoubleClick) ? "cursor-pointer" : ""}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>

View File

@@ -186,6 +186,7 @@
{loading}
{hasMore}
{loadMore}
onRowDoubleClick={(item) => goto(`/dashboard/general_catalogs/company_information/edit/${item.id}`)}
/>
</div>
</Card.Content>

View File

@@ -276,6 +276,7 @@
{loadMore}
{selectedId}
onRowClick={handleRowClick}
onRowDoubleClick={(item) => goto(`/dashboard/general_catalogs/doda/edit/${item.id}`)}
/>
</div>
</Card.Content>

View File

@@ -31,6 +31,7 @@
// Estado de la lista de clases
let classes = $state<FixedAssetClassExtended[]>([]);
let selectedClassIds = $state<number[]>([]);
let selectedClass = $state<FixedAssetClassExtended | null>(null);
let isLoading = $state(false);
let searchTerm = $state('');
@@ -107,6 +108,9 @@
if (response.data) {
// Data already comes with FA fields embedded
classes = response.data as FixedAssetClassExtended[];
handleSelectedIdsChange(
selectedClassIds.filter((id) => classes.some((item) => item.id === id))
);
}
} catch (error) {
console.error('Error cargando clases:', error);
@@ -116,7 +120,30 @@
}
}
function selectClass(cls: A76Class) {
function clearSelectedClass() {
selectedClass = null;
formData = {
class_code: '',
description_es: '',
description_en: '',
material_key: '',
unit_of_measure: '',
fraction: '',
us_fraction: '',
unit_measure_trade: '',
depreciation_rate: null as number | null,
fda_code: '',
eccn_code: '',
bom: ''
};
}
function setActiveClass(cls: FixedAssetClassExtended | null) {
if (!cls) {
clearSelectedClass();
return;
}
selectedClass = cls;
formData = {
class_code: cls.class_code,
@@ -134,22 +161,27 @@
};
}
function handleSelectedIdsChange(ids: number[]) {
selectedClassIds = ids;
const primaryId = ids[0];
const primaryClass = primaryId
? classes.find((item) => item.id === primaryId) ?? null
: null;
setActiveClass(primaryClass);
}
function handleRowDoubleClick(cls: A76Class) {
const fixedClass = cls as FixedAssetClassExtended;
selectedClassIds = [fixedClass.id, ...selectedClassIds.filter((id) => id !== fixedClass.id)];
setActiveClass(fixedClass);
validationError = '';
showInsertDialog = true;
}
function handleNew() {
selectedClass = null;
formData = {
class_code: '',
description_es: '',
description_en: '',
material_key: '',
unit_of_measure: '',
fraction: '',
us_fraction: '',
unit_measure_trade: '',
depreciation_rate: null as number | null,
fda_code: '',
eccn_code: '',
bom: ''
};
selectedClassIds = [];
clearSelectedClass();
}
async function handleRefresh() {
@@ -185,22 +217,9 @@
// Recargar lista
await loadClasses();
selectedClass = null;
selectedClassIds = [];
clearSelectedClass();
showDeleteDialog = false;
formData = {
class_code: '',
description_es: '',
description_en: '',
material_key: '',
unit_of_measure: '',
fraction: '',
us_fraction: '',
unit_measure_trade: '',
depreciation_rate: null as number | null,
fda_code: '',
eccn_code: '',
bom: ''
};
} catch (error) {
console.error('Error deleting class:', error);
toast.error('Error al eliminar la clase');
@@ -318,8 +337,9 @@
data={filteredClasses}
{columns}
loading={isLoading}
selectedId={selectedClass?.id}
onRowClick={selectClass}
selectedIds={selectedClassIds}
onSelectedIdsChange={handleSelectedIdsChange}
onRowDoubleClick={handleRowDoubleClick}
{sorting}
onSortingChange={(newSorting) => (sorting = newSorting)}
/>
@@ -610,7 +630,8 @@
const wasUpdate = !!selectedClass?.id;
await loadClasses();
showInsertDialog = false;
selectedClass = null;
selectedClassIds = [];
clearSelectedClass();
validationError = '';
toast.success(
wasUpdate ? 'Clase actualizada correctamente' : 'Clase creada correctamente'

View File

@@ -303,6 +303,7 @@
{sorting}
onSortingChange={(newSorting) => (sorting = newSorting)}
onRowClick={selectPart}
onRowDoubleClick={(part) => goto(`/dashboard/goods/parts/edit/${part.id}`)}
/>
</div>
</div>