feature/catalogos-generales-permisos

This commit is contained in:
2026-05-06 11:13:09 -06:00
parent 12dd328eb5
commit e3ad7e7a7e
118 changed files with 1318 additions and 324 deletions

View File

@@ -70,12 +70,14 @@
let {
dodaIdParam,
defaultLastUser: defaultLastUserProp = '',
canMutate = true,
onClose,
onCreatedNavigateTo
}: {
/** 'new' o id numérico en string; desde ?doda_id= de la URL */
dodaIdParam: string;
defaultLastUser?: string;
canMutate?: boolean;
onClose: () => void;
/** Tras crear DODA, actualiza ?doda_id= al id creado (sin desmontar la lista) */
onCreatedNavigateTo: (dodaId: number) => void;
@@ -170,13 +172,22 @@
dodaFormT(dodaLoc, 'shortcuts_scope'),
obtenerAtajosFormularioDodaPagina({
cambiarPestana: (pestana) => (activeTab = pestana),
manejarGuardar: handleSubmit,
manejarGuardar: () => {
if (!canMutate) return;
handleSubmit();
},
manejarCerrar: () => {
if (browser) onClose();
}
})
);
function ensureCanMutate(): boolean {
if (canMutate) return true;
error = 'Permission denied: cat_doda.create/edit';
return false;
}
function getEmptyForm(): DodaCreate & {
pedimentos_detail?: any[];
containers?: any[];
@@ -400,6 +411,7 @@
}
async function handleAddSeal() {
if (!ensureCanMutate()) return;
error = null;
if (!isEdit || !id) {
error = t('seal_save_first');
@@ -457,6 +469,7 @@
}
async function handleAddSealFromContainerModal() {
if (!ensureCanMutate()) return;
const trimmed = newSealValue.trim();
if (!trimmed) {
error = t('seal_empty');
@@ -493,6 +506,7 @@
}
function handleAmericanPedimentoModalConfirm() {
if (!ensureCanMutate()) return;
error = null;
const tipo = americanModalType.trim();
const valor = americanModalValue.trim();
@@ -539,6 +553,7 @@
}
async function handleDeleteSeal(sealLine: number) {
if (!ensureCanMutate()) return;
error = null;
if (!isEdit || !id) return;
if (selectedContainerIndex == null) return;
@@ -563,6 +578,7 @@
}
async function handleSubmit() {
if (!ensureCanMutate()) return;
submitAttempted = true;
error = null;
warning = null;
@@ -1621,7 +1637,7 @@
<Button
size="sm"
onclick={handleSubmit}
disabled={loading}
disabled={loading || !canMutate}
class="h-8 min-w-[7rem] px-4 text-sm font-semibold"
>
{#if loading}

View File

@@ -15,10 +15,12 @@
let {
open = $bindable(false),
item = null,
canMutate = true,
onSuccess
}: {
open: boolean;
item?: Location | null;
canMutate?: boolean;
onSuccess?: () => void;
} = $props();
@@ -59,6 +61,10 @@
});
async function handleSubmit() {
if (!canMutate) {
error = 'Permission denied';
return;
}
loading = true;
error = null;
try {
@@ -134,7 +140,7 @@
id="clave_localizacion"
bind:value={formData.clave_localizacion}
maxlength={20}
disabled={loading || isEdit}
disabled={loading || isEdit || !canMutate}
required
/>
</div>
@@ -147,7 +153,7 @@
id="localizacion"
bind:value={formData.localizacion}
maxlength={200}
disabled={loading}
disabled={loading || !canMutate}
/>
</div>
</div>
@@ -158,7 +164,7 @@
<select
id="system"
bind:value={formData.system}
disabled={loading}
disabled={loading || !canMutate}
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{#each systemOptions as opt}
@@ -173,7 +179,7 @@
<Button type="button" variant="outline" onclick={() => (open = false)} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
<Button type="submit" disabled={loading || !canMutate}>
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
</Button>
</Dialog.Footer>

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { userHasSectorsCatalogAction } from '$lib/permissions/catalogs/sectors-catalog-permissions';
import { Input } from '$lib/components/ui/input';
import * as Table from '$lib/components/ui/table';
import * as Card from '$lib/components/ui/card';
@@ -10,7 +12,6 @@
import { companyStore } from '$lib/stores/company.svelte';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import { currentUser, userHasPermission } from '$lib/auth';
let { title = 'Sectores' }: { title?: string } = $props();
let sectors = $state<Sector[]>([]);
@@ -19,10 +20,10 @@
let status = $state<number>(200);
// Permisos
const canView = $derived(userHasPermission($currentUser, 'cat_sectors.view') || userHasPermission($currentUser, 'frac_sectors.view'));
const canCreate = $derived(userHasPermission($currentUser, 'cat_sectors.create'));
const canEdit = $derived(userHasPermission($currentUser, 'cat_sectors.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'cat_sectors.delete'));
const canView = $derived(userHasPermission($currentUser, 'ref_sectors.view'));
const canCreate = $derived(userHasSectorsCatalogAction($currentUser, 'create'));
const canEdit = $derived(userHasSectorsCatalogAction($currentUser, 'edit'));
const canDelete = $derived(userHasSectorsCatalogAction($currentUser, 'delete'));
const isError = $derived(!canView || status >= 400 || error);
let searchTerm = $state('');

View File

@@ -8,7 +8,11 @@ const SYSTEM_LABELS: Record<string, string> = {
inventory: 'Inventario'
};
export function createColumns(onSuccess?: () => void): ColumnDef<Location>[] {
export function createColumns(
onSuccess?: () => void,
options: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<Location>[] {
const { canEdit = true, canDelete = true } = options;
return [
{
accessorKey: 'clave_localizacion',
@@ -30,7 +34,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Location>[] {
cell: ({ row }) =>
renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
})
}
];

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Location;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -21,6 +25,7 @@
let selectedItem = $state<Location | null>(null);
async function handleDelete() {
if (!canDelete) return;
if (!confirm('¿Está seguro de que desea eliminar esta ubicación?')) {
return;
}
@@ -49,6 +54,7 @@
}
function handleEdit() {
if (!canEdit) return;
selectedItem = item;
dialogOpen = true;
}
@@ -74,12 +80,12 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<DropdownMenu.Item onclick={handleEdit} disabled={!canEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading || !canDelete}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
@@ -93,5 +99,6 @@
<CreateEditDialog
bind:open={dialogOpen}
item={selectedItem}
canMutate={canEdit}
onSuccess={handleDialogSuccess}
/>

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { userHasLocationsCatalogAction } from '$lib/permissions/catalogs/locations-catalog-permissions';
import { createColumns } from '$lib/components/dashboard/locations/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
@@ -10,7 +12,8 @@
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import { currentUser } from '$lib/auth';
let {
companyId,
system: initialSystem = undefined,
@@ -32,14 +35,27 @@
let pageSize = $state(50);
let pages = $state(0);
let loading = $state(true);
let status = $state(200);
let dialogOpen = $state(false);
let searchClave = $state('');
let searchLocalizacion = $state('');
let filterSystem = $state<LocationSystem | ''>(initialSystem ?? '');
let timeout: ReturnType<typeof setTimeout>;
const canView = $derived(userHasLocationsCatalogAction($currentUser, 'view'));
const canCreate = $derived(userHasLocationsCatalogAction($currentUser, 'create'));
const canEdit = $derived(userHasLocationsCatalogAction($currentUser, 'edit'));
const canDelete = $derived(userHasLocationsCatalogAction($currentUser, 'delete'));
async function load() {
if (!companyId) return;
if (!canView) {
loading = false;
items = [];
total = 0;
pages = 0;
status = 403;
return;
}
loading = true;
try {
const filters: Record<string, string | number> = {
@@ -52,6 +68,7 @@
const res = await getLocations(companyId, filters);
items = res.items ?? [];
total = res.total ?? 0;
status = 200;
page = res.page ?? 1;
pageSize = res.page_size ?? 50;
pages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
@@ -60,6 +77,7 @@
items = [];
total = 0;
pages = 0;
status = 500;
} finally {
loading = false;
}
@@ -80,11 +98,15 @@
$effect(() => {
void companyId;
void page;
void canView;
load();
});
</script>
<div class="flex flex-col gap-4">
{#if !canView}
<ErrorState status={403} error="Permission denied: cat_locations.view" />
{:else}
{#if !compact}
<div class="flex items-center justify-between">
<div>
@@ -93,14 +115,14 @@
Clave y localización por sistema (FA / Inventario)
</p>
</div>
<Button onclick={() => (dialogOpen = true)} size="sm">
<Button onclick={() => (dialogOpen = true)} size="sm" disabled={!canCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva
</Button>
</div>
{:else}
<div class="flex items-center justify-end">
<Button onclick={() => (dialogOpen = true)} size="sm" variant="outline">
<Button onclick={() => (dialogOpen = true)} size="sm" variant="outline" disabled={!canCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva ubicación
</Button>
@@ -145,12 +167,13 @@
{:else}
<DataTable
data={items}
columns={createColumns(handleSuccess)}
columns={createColumns(handleSuccess, { canEdit, canDelete })}
pageCount={pages}
totalItems={total}
/>
{/if}
</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} canMutate={canCreate} />
{/if}
</div>

View File

@@ -198,26 +198,32 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.general_catalogs.company_information"](),
url: "/dashboard/general_catalogs/company_information",
permission: "cat_company.view",
},
{
title: m["sidebar.general_catalogs.packages"](),
url: "/dashboard/general_catalogs/packages",
permission: "cat_packages.view",
},
{
title: m["sidebar.general_catalogs.concepts"](),
url: "/dashboard/general_catalogs/concepts",
permission: "cat_concepts.view",
},
{
title: m["sidebar.general_catalogs.customs_broker_concepts"](),
url: "/dashboard/general_catalogs/customs_broker_concepts",
permission: "cat_broker_concepts.view",
},
{
title: m["sidebar.general_catalogs.classification"](),
url: "/dashboard/general_catalogs/classification_concepts",
permission: "cat_classification.view",
},
{
title: m["sidebar.general_catalogs.identifiers"](),
url: "/dashboard/general_catalogs/identifiers",
permission: "cat_identifiers.view",
},
// -------------------------------------
{
@@ -228,14 +234,17 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.general_catalogs.inpc"](),
url: "/dashboard/general_catalogs/inpc",
permission: "cat_inpc.view",
},
{
title: m["sidebar.general_catalogs.fixed_legends"](),
url: "/dashboard/general_catalogs/legends",
permission: "cat_legends.view",
},
{
title: m["sidebar.general_catalogs.seals"](),
url: "/dashboard/general_catalogs/seal",
permission: "cat_seals.view",
},
{
title: m["sidebar.general_catalogs.valuation_methods"](),
@@ -250,26 +259,37 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.general_catalogs.ports"](),
url: "/dashboard/general_catalogs/ports",
permission: "cat_ports.view",
},
{
title: "Ubicaciones",
url: "/dashboard/general_catalogs/locations",
permission: "cat_locations.view",
},
{
title: m["sidebar.general_catalogs.unit_measures"](),
url: "/dashboard/general_catalogs/units_of_measure/general",
permission: "cat_um_general.view",
},
{
title: m["sidebar.general_catalogs.um_customs_mex"](),
url: "/dashboard/general_catalogs/units_of_measure/customs",
permission: "cat_um_customs.view",
},
{
title: m["sidebar.general_catalogs.um_customs_ame"](),
url: "/dashboard/general_catalogs/units_of_measure/american",
permission: "cat_um_american.view",
},
{
title: m["sidebar.general_catalogs.um_ace"](),
url: "/dashboard/general_catalogs/units_of_measure/ace",
permission: "cat_um_ace.view",
},
{
title: m["sidebar.general_catalogs.um_oma"](),
url: "/dashboard/general_catalogs/units_of_measure/oma",
permission: "cat_um_oma.view",
},
{
title: m["sidebar.general_catalogs.conversions"](),
@@ -304,10 +324,12 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.general_catalogs.electronic_signatures"](),
url: "/dashboard/general_catalogs/signatures",
permission: "cat_signatures.view",
},
{
title: m["sidebar.general_catalogs.billing_errors"](),
url: "/dashboard/general_catalogs/error_catalogs",
permission: "cat_errors.view",
},
{
title: m["sidebar.general_catalogs.customs_warehouses"](),
@@ -317,14 +339,12 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.general_catalogs.prevalidators"](),
url: "/dashboard/general_catalogs/prevalidators",
permission: "cat_prevalidators.view",
},
{
title: m["sidebar.general_catalogs.electronic_notices"](),
url: "/dashboard/general_catalogs/electronic_notices",
},
{
title: m["sidebar.general_catalogs.crossing_notice"](),
url: "#",
permission: "cat_notices.view",
},
],
},
@@ -361,7 +381,7 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.fractions.sectors"](),
url: "/dashboard/general_catalogs/sectors",
permission: 'frac_sectors.view',
permission: 'ref_sectors.view',
},
],
},