feature/mercancias-permisos

This commit is contained in:
2026-05-05 15:05:11 -06:00
parent 63939e3aa7
commit bc7160790b
12 changed files with 307 additions and 45 deletions

View File

@@ -18,7 +18,16 @@ function formatCurrency(amount: number | null, currency: string | null): string
}).format(amount);
}
export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
export type GoodsPartsColumnOptions = {
canEdit?: boolean;
canDelete?: boolean;
};
export function createColumns(
onSuccess?: () => void,
options: GoodsPartsColumnOptions = {}
): ColumnDef<Part>[] {
const { canEdit = false, canDelete = false } = options;
return [
{
id: "select",
@@ -301,10 +310,13 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
id: "actions",
header: "",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit,
canDelete
});
}
}
];
}
export const columns = createColumns();
}

View File

@@ -9,16 +9,21 @@
let {
item,
onSuccess
onSuccess,
canEdit = false,
canDelete = false
}: {
item: Part; // Cambiado a Part
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
async function handleDelete() {
if (!canDelete) return;
// 2. Ajustamos el mensaje de confirmación para que muestre el No. Parte
if (!confirm(`¿Estás seguro de eliminar la parte "${item.part_number}"?`)) {
return;
@@ -64,11 +69,13 @@
}
function handleEdit() {
if (!canEdit) return;
// Redirigimos a la página de edición usando el ID
goto(`/dashboard/goods/parts/edit/${item.id}`);
}
</script>
{#if canEdit || canDelete}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
@@ -81,14 +88,14 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
{#if canEdit}
<DropdownMenu.Item onSelect={handleEdit}>
<Pencil size={16} class="mr-2" />
<span>Editar</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
{/if}
{#if canDelete}
{#if canEdit}<DropdownMenu.Separator />{/if}
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
@@ -97,5 +104,7 @@
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root>
{/if}

View File

@@ -367,10 +367,12 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.goods.classes"](),
url: "/dashboard/goods/fixed-asset-classes",
permission: 'goods_classes.view',
},
{
title: m["sidebar.goods.parts"](),
url: "/dashboard/goods/parts",
permission: 'goods_parts.view',
},
{
title: m["sidebar.goods.fda_codes"](),

View File

@@ -0,0 +1,23 @@
import type { User } from '$lib/auth';
import { userHasPermission } from '$lib/auth';
function hasAssignedCode(user: User | null, code: string): boolean {
if (!user) return false;
return user.permissions.includes(code);
}
export function canViewGoodsClasses(user: User | null): boolean {
return userHasPermission(user, 'goods_classes.view');
}
export function canCreateGoodsClasses(user: User | null): boolean {
return hasAssignedCode(user, 'goods_classes.create');
}
export function canEditGoodsClasses(user: User | null): boolean {
return hasAssignedCode(user, 'goods_classes.edit');
}
export function canDeleteGoodsClasses(user: User | null): boolean {
return hasAssignedCode(user, 'goods_classes.delete');
}

View File

@@ -0,0 +1,23 @@
import type { User } from '$lib/auth';
import { userHasPermission } from '$lib/auth';
function hasAssignedCode(user: User | null, code: string): boolean {
if (!user) return false;
return user.permissions.includes(code);
}
export function canViewGoodsParts(user: User | null): boolean {
return userHasPermission(user, 'goods_parts.view');
}
export function canCreateGoodsParts(user: User | null): boolean {
return hasAssignedCode(user, 'goods_parts.create');
}
export function canEditGoodsParts(user: User | null): boolean {
return hasAssignedCode(user, 'goods_parts.edit');
}
export function canDeleteGoodsParts(user: User | null): boolean {
return hasAssignedCode(user, 'goods_parts.delete');
}

View File

@@ -11,8 +11,14 @@
import { companyStore } from '$lib/stores/company.svelte';
import DataTable from '$lib/components/dashboard/goods/classes/data-table.svelte';
import { columns } from '$lib/components/dashboard/goods/classes/columns';
import { currentUser, userHasPermission } from '$lib/auth';
import { currentUser } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import {
canCreateGoodsClasses,
canDeleteGoodsClasses,
canEditGoodsClasses,
canViewGoodsClasses
} from '$lib/permissions/goods-classes-permissions';
import {
formatMexTariffDigitsForDisplay,
normalizeMexTariffDigitsStored
@@ -62,10 +68,10 @@
let status = $state<number>(200);
// Permisos
const canView = $derived(userHasPermission($currentUser, 'goods_classes.view'));
const canCreate = $derived(userHasPermission($currentUser, 'goods_classes.create'));
const canEdit = $derived(userHasPermission($currentUser, 'goods_classes.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'goods_classes.delete'));
const canView = $derived(canViewGoodsClasses($currentUser));
const canCreate = $derived(canCreateGoodsClasses($currentUser));
const canEdit = $derived(canEditGoodsClasses($currentUser));
const canDelete = $derived(canDeleteGoodsClasses($currentUser));
const isError = $derived(!canView || status >= 400);
@@ -259,6 +265,10 @@
}
function handleRowDoubleClick(cls: A76Class) {
if (!canEdit) {
toast.error('No tienes permiso para editar clases de activo fijo');
return;
}
const fixedClass = cls as FixedAssetClassExtended;
selectedClassIds = [fixedClass.id, ...selectedClassIds.filter((id) => id !== fixedClass.id)];
setActiveClass(fixedClass);
@@ -347,6 +357,15 @@
description: 'Guardar clase',
action: () => {
if (!showInsertDialog || isSaving) return;
const isEditMode = !!selectedClass?.id;
if (isEditMode && !canEdit) {
toast.error('No tienes permiso para editar clases');
return;
}
if (!isEditMode && !canCreate) {
toast.error('No tienes permiso para crear clases');
return;
}
document.dispatchEvent(new CustomEvent('save-form'));
}
}
@@ -658,6 +677,14 @@
if (isSaving) {
return;
}
if (selectedClass?.id && !canEdit) {
toast.error('No tienes permiso para editar clases');
return;
}
if (!selectedClass?.id && !canCreate) {
toast.error('No tienes permiso para crear clases');
return;
}
isSaving = true;
validationError = '';

View File

@@ -11,8 +11,14 @@
import DataTable from '$lib/components/dashboard/goods/parts/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/goods/parts/columns';
import { obtenerAtajosListaMercancias } from '$lib/config/shortcuts/dashboard/goods/list';
import { currentUser, userHasPermission } from '$lib/auth';
import { currentUser } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import {
canCreateGoodsParts,
canDeleteGoodsParts,
canEditGoodsParts,
canViewGoodsParts
} from '$lib/permissions/goods-parts-permissions';
import { formatMexTariffDigitsForDisplay } from '$lib/utils/mexican-tariff-fraction';
// Estado de la lista de partes
@@ -26,16 +32,21 @@
{ id: 'updated_at', desc: true }
]);
// Pasamos los permisos a createColumns para bloquear las acciones de fila internamente
const columns = createColumns(() => loadParts());
let error = $state<string | null>(null);
let status = $state<number>(200);
// Permisos
const canView = $derived(userHasPermission($currentUser, 'goods_parts.view'));
const canCreate = $derived(userHasPermission($currentUser, 'goods_parts.create'));
const canEdit = $derived(userHasPermission($currentUser, 'goods_parts.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'goods_parts.delete'));
const canView = $derived(canViewGoodsParts($currentUser));
const canCreate = $derived(canCreateGoodsParts($currentUser));
const canEdit = $derived(canEditGoodsParts($currentUser));
const canDelete = $derived(canDeleteGoodsParts($currentUser));
const columns = $derived(
createColumns(() => loadParts(), {
canEdit,
canDelete
})
);
const isError = $derived(!canView || status >= 400 || error);
@@ -67,16 +78,26 @@
'Goods List',
obtenerAtajosListaMercancias({
crear: () => {
if (canCreate) goto('/dashboard/goods/parts/edit/new');
if (!canCreate) {
toast.error('No tienes permiso para crear partes');
return;
}
goto('/dashboard/goods/parts/edit/new');
},
recargar: loadData,
editar: () => {
if (!canEdit) return;
if (!canEdit) {
toast.error('No tienes permiso para editar partes');
return;
}
if (selectedPart?.id) goto(`/dashboard/goods/parts/edit/${selectedPart.id}`);
else toast.info('Seleccione una parte para editar');
},
eliminar: () => {
if (!canDelete) return;
if (!canDelete) {
toast.error('No tienes permiso para eliminar partes');
return;
}
if (selectedPart?.id) handleDelete();
else toast.info('Seleccione una parte para eliminar');
}
@@ -253,7 +274,13 @@
{sorting}
onSortingChange={(newSorting) => (sorting = newSorting)}
onRowClick={selectPart}
onRowDoubleClick={(part) => canEdit && goto(`/dashboard/goods/parts/edit/${part.id}`)}
onRowDoubleClick={(part) => {
if (!canEdit) {
toast.error('No tienes permiso para editar partes');
return;
}
goto(`/dashboard/goods/parts/edit/${part.id}`);
}}
/>
</div>
</div>

View File

@@ -3,8 +3,19 @@
import { goto } from '$app/navigation';
import PartForm from '$lib/components/dashboard/goods/parts/partForm.svelte';
import PrerequisitesModal from '$lib/components/dashboard/PrerequisitesModal.svelte';
import { currentUser } from '$lib/auth';
import {
canCreateGoodsParts,
canEditGoodsParts
} from '$lib/permissions/goods-parts-permissions';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
let routeId = $derived($page.params.id);
let id = $derived($page.params.id === 'new' ? null : Number($page.params.id));
let isEditingPart = $derived(!!routeId && routeId !== 'new');
const canAccessPartEditor = $derived(
isEditingPart ? canEditGoodsParts($currentUser) : canCreateGoodsParts($currentUser)
);
let type = $state<'inv' | 'fa' | 'both'>('fa');
// When editing, the PartForm auto-detects the type from the loaded data.
@@ -27,6 +38,13 @@
});
</script>
{#if !canAccessPartEditor}
<div
class="flex h-[calc(100svh-4rem)] flex-col p-6 group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)]"
>
<ErrorState status={403} />
</div>
{:else}
{#if data?.isCreate}
<PrerequisitesModal
bind:open={showPrerequisitesModal}
@@ -40,3 +58,4 @@
<div class="space-y-4 p-4">
<PartForm partId={id} bind:formType={type} />
</div>
{/if}