Botones de edicion y eliminacion en el footer de la informacion de los secotres y doble click para ver la informacion

This commit is contained in:
2026-05-15 12:28:23 -05:00
parent 76b03b2938
commit e919cc60a3
4 changed files with 272 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
import type { Sector } from '$lib/api/dashboard/general_catalogs/sectors';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(
onSuccess?: () => void,
permissions?: { canEdit: boolean; canDelete: boolean }
): ColumnDef<Sector>[] {
return [
{
accessorKey: 'key',
header: 'Clave',
cell: ({ row }) => row.original.key || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'authorized',
header: 'Estatus',
cell: ({ row }) => (row.original.authorized ? 'Autorizado' : 'No Autorizado')
},
{
id: 'actions',
header: '',
cell: ({ row }) =>
renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions?.canEdit ?? false,
canDelete: permissions?.canDelete ?? false
})
}
];
}

View File

@@ -0,0 +1,110 @@
<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 { Switch } from '$lib/components/ui/switch';
import { sectorsApi, type Sector } from '$lib/api/dashboard/reference_data/sectors';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
item = null,
onSuccess
}: {
open: boolean;
item?: Sector | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? 'Editar Sector' : 'Nuevo Sector');
let formData = $state({ key: '', description: '', authorized: false });
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (!open) return;
if (item) {
formData = { key: item.key || '', description: item.description || '', authorized: item.authorized ?? false };
} else {
formData = { key: '', description: '', authorized: false };
}
error = null;
});
async function handleSubmit() {
error = null;
loading = true;
if (!companyStore.activeCompany) {
error = 'No hay una empresa seleccionada.';
loading = false;
return;
}
const companyId = companyStore.activeCompany.id;
try {
if (!formData.key.trim()) throw new Error('La clave es requerida');
if (!formData.description.trim()) throw new Error('La descripción es requerida');
const payload = {
key: formData.key.trim(),
description: formData.description.trim(),
authorized: formData.authorized
};
const response = isEdit && item
? await sectorsApi.update(item.id, payload, companyId)
: await sectorsApi.create(payload, companyId);
if (response.error) throw new Error(response.error);
open = false;
onSuccess?.();
} catch (e) {
error = e instanceof Error ? e.message : 'Error al guardar';
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-2">
{#if error}
<div class="rounded-md border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="grid gap-2">
<Label for="key">Clave <span class="text-destructive">*</span></Label>
<Input id="key" bind:value={formData.key} maxlength={8} placeholder="Ej: XIX" />
</div>
<div class="grid gap-2">
<Label for="description">Descripción <span class="text-destructive">*</span></Label>
<Input id="description" bind:value={formData.description} maxlength={150} />
</div>
<div class="flex items-center gap-2 pt-1">
<Switch id="authorized" bind:checked={formData.authorized} />
<Label for="authorized">Autorizado para PROSEC</Label>
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { sectorsApi, type Sector } from '$lib/api/dashboard/reference_data/sectors';
import { companyStore } from '$lib/stores/company.svelte';
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
let {
item,
onSuccess,
canEdit = false,
canDelete = false
}: {
item: Sector;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el sector "${item.key}"?`)) return;
if (!companyStore.activeCompany) return;
loading = true;
try {
const response = await sectorsApi.delete(item.id, companyStore.activeCompany.id);
if (!response.error) onSuccess?.();
} finally {
loading = false;
}
}
</script>
{#if canEdit || canDelete}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
{#if canEdit}
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
{#if canEdit}<DropdownMenu.Separator />{/if}
<DropdownMenu.Item
class="text-destructive focus:text-destructive"
onclick={handleDelete}
disabled={loading}
>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog bind:open={dialogOpen} {item} {onSuccess} />
{/if}

View File

@@ -30,6 +30,14 @@
let error = $state<string | null>(data.error || null);
let status = $state<number>(data.status || 200);
// Selección de fila
let selectedIds = $state<(string | number)[]>([]);
const selectedItem = $derived(
selectedIds.length === 1
? (allItems.find((s) => String(s.id) === String(selectedIds[0])) ?? null)
: null
);
// Diálogos
let createDialogOpen = $state(false);
let deleteDialogOpen = $state(false);
@@ -181,12 +189,52 @@
</Card.Header>
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
<InfiniteDataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
<InfiniteDataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
{selectedIds}
onSelectedIdsChange={(ids) => (selectedIds = ids)}
onRowClick={(row) => (selectedIds = selectedIds.includes(row.id) ? [] : [row.id])}
onRowDoubleClick={(row) => { if (canEdit) handleEdit(row); }}
/>
</div>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
<div
class="fixed right-0 bottom-0 left-0 z-50 ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
>
<div class="mx-auto max-w-[1400px] px-4 py-4">
<div class="flex justify-end gap-2">
{#if canEdit}
<Button
variant="outline"
size="sm"
onclick={() => selectedItem && handleEdit(selectedItem)}
disabled={!selectedItem}
>
<Pencil size={16} class="mr-2" /> Editar
</Button>
{/if}
{#if canDelete}
<Button
variant="outline"
size="sm"
onclick={() => selectedItem && handleDelete(selectedItem)}
disabled={!selectedItem}
class="text-destructive hover:bg-destructive/10"
>
<Trash2 size={16} class="mr-2" /> Eliminar
</Button>
{/if}
</div>
</div>
</div>
{/if}
</div>