feature/mercancias-permisos
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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"](),
|
||||
|
||||
23
frontend/src/lib/permissions/goods-classes-permissions.ts
Normal file
23
frontend/src/lib/permissions/goods-classes-permissions.ts
Normal 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');
|
||||
}
|
||||
23
frontend/src/lib/permissions/goods-parts-permissions.ts
Normal file
23
frontend/src/lib/permissions/goods-parts-permissions.ts
Normal 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');
|
||||
}
|
||||
@@ -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 = '';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user