feature/permisos-pedimentos
This commit is contained in:
@@ -386,22 +386,27 @@ export function getSidebarData(): SidebarData {
|
||||
{
|
||||
title: m["sidebar.pedimentos.pedimento_management"](),
|
||||
url: "/dashboard/pedimentos",
|
||||
permission: 'pedimentos_mgmt.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.pedimentos.pedimento_codes"](),
|
||||
url: "/dashboard/reference_data/pedimento_codes",
|
||||
permission: 'ref_pedimento_codes.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.pedimentos.customs_regimes"](),
|
||||
url: "/dashboard/reference_data/pedimento_regimens",
|
||||
permission: 'ref_pedimento_regimens.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.pedimentos.payment_methods"](),
|
||||
url: "/dashboard/reference_data/payment_methods",
|
||||
permission: 'pedimentos_payments.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.pedimentos.customs_sections"](),
|
||||
url: "/dashboard/reference_data/customs_sections",
|
||||
permission: 'ref_customs_sections.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.pedimentos.anexo_22_app_31"](),
|
||||
|
||||
43
frontend/src/lib/permissions/pedimento-permissions.ts
Normal file
43
frontend/src/lib/permissions/pedimento-permissions.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Permisos de pedimentos — códigos `pedimentos_mgmt.*` (seed / API).
|
||||
*
|
||||
* - **Ver listado:** `userHasPermission` (incluye comodín de rol Keycloak `admin` como en otras pantallas).
|
||||
* - **Crear / editar / borrar (mutaciones):** solo códigos en `user.permissions` (tras `syncCompanyPermissions`).
|
||||
*
|
||||
* **Alta de pedimento (`Nuevo` / `/edit/new`):** misma regla que editar — `pedimentos_mgmt.edit`
|
||||
* (negocio: alta y edición son la misma capacidad).
|
||||
*/
|
||||
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 canViewPedimentosList(user: User | null): boolean {
|
||||
return userHasPermission(user, 'pedimentos_mgmt.view');
|
||||
}
|
||||
|
||||
/** Permiso de API/rol para POST crear pedimento; el botón "Nuevo" en UI usa `canOpenNuevoPedimentoForm` (edit). */
|
||||
export function canCreatePedimento(user: User | null): boolean {
|
||||
return hasAssignedCode(user, 'pedimentos_mgmt.create');
|
||||
}
|
||||
|
||||
export function canEditPedimento(user: User | null): boolean {
|
||||
return hasAssignedCode(user, 'pedimentos_mgmt.edit');
|
||||
}
|
||||
|
||||
export function canDeletePedimento(user: User | null): boolean {
|
||||
return hasAssignedCode(user, 'pedimentos_mgmt.delete');
|
||||
}
|
||||
|
||||
/** Botón "Nuevo Pedimento" y navegación a `/edit/new` — requiere poder editar en RBAC de la app. */
|
||||
export function canOpenNuevoPedimentoForm(user: User | null): boolean {
|
||||
return canEditPedimento(user);
|
||||
}
|
||||
|
||||
/** Pantalla de alta (`/edit/new`) o edición — ambas exigen `pedimentos_mgmt.edit` en cliente (alineado a API). */
|
||||
export function canAccessPedimentoEditorPage(user: User | null, _isCreate: boolean): boolean {
|
||||
return canEditPedimento(user);
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import {
|
||||
getAuthTokens,
|
||||
authenticatedFetch
|
||||
} from '$lib/server/api';
|
||||
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
const parentData = await parent();
|
||||
|
||||
|
||||
// Verificar autenticación
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
|
||||
if (!accessToken) {
|
||||
throw redirect(302, '/login');
|
||||
}
|
||||
@@ -23,13 +20,13 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
|
||||
// 3. Primera compañía del usuario (fallback)
|
||||
const companyIdParam = url.searchParams.get('company_id');
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
|
||||
const companyId = companyIdParam
|
||||
|
||||
const companyId = companyIdParam
|
||||
? parseInt(companyIdParam)
|
||||
: cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
|
||||
// Si aún no hay companyId, mostrar error
|
||||
if (!companyId) {
|
||||
return {
|
||||
@@ -41,8 +38,8 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
|
||||
companies: parentData.companies || []
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
|
||||
// Misma idea que facturas: lista desde API; permisos en cliente vía syncCompanyPermissions + userHasPermission
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/pedimentos?company_id=${companyId}&page=1&page_size=50`,
|
||||
{},
|
||||
@@ -52,12 +49,17 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
response.status === 403
|
||||
? 'No tiene permiso para ver pedimentos'
|
||||
: 'Error al cargar pedimentos';
|
||||
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
error: 'Error al cargar pedimentos',
|
||||
error: message,
|
||||
companies: parentData.companies || [],
|
||||
currentCompanyId: companyId
|
||||
};
|
||||
|
||||
@@ -21,10 +21,22 @@
|
||||
import { reportsWinsaaiApi } from '$lib/api/dashboard/a76/reports/reports-winsaai';
|
||||
import PdfProgressDialog from '$lib/components/dashboard/invoices/pdf-progress-dialog.svelte';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { get } from 'svelte/store';
|
||||
import { currentUser } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
import {
|
||||
canViewPedimentosList,
|
||||
canEditPedimento,
|
||||
canDeletePedimento,
|
||||
canOpenNuevoPedimentoForm
|
||||
} from '$lib/permissions/pedimento-permissions';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const canViewList = $derived(canViewPedimentosList($currentUser));
|
||||
const canNuevoPedimentoAction = $derived(canOpenNuevoPedimentoForm($currentUser));
|
||||
|
||||
// Estado para filtros
|
||||
let filters = $state({
|
||||
status: '',
|
||||
@@ -69,6 +81,14 @@ let filters = $state({
|
||||
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
|
||||
void (async () => {
|
||||
const cid = companyStore.activeCompany?.id;
|
||||
if (cid) {
|
||||
const { syncCompanyPermissions } = await import('$lib/auth');
|
||||
await syncCompanyPermissions(cid);
|
||||
}
|
||||
})();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
@@ -95,7 +115,6 @@ let filters = $state({
|
||||
|
||||
// Estado para selección de filas
|
||||
let selectedId = $state<number | null>(null);
|
||||
let hasSelection = $derived(selectedId !== null);
|
||||
let showDeleteDialog = $state(false);
|
||||
let isWinsaiiConfirmOpen = $state(false);
|
||||
let isWinsaiiByClass = $state(false);
|
||||
@@ -107,6 +126,12 @@ let filters = $state({
|
||||
let currentStatusFunction = $state<any>(null);
|
||||
|
||||
let selectedPedimento = $derived(allItems.find((p) => p.id === selectedId) || null);
|
||||
const canEditSelected = $derived(
|
||||
selectedPedimento ? canEditPedimento($currentUser) : false
|
||||
);
|
||||
const canDeleteSelected = $derived(
|
||||
selectedPedimento ? canDeletePedimento($currentUser) : false
|
||||
);
|
||||
|
||||
function handleRowClick(pedimento: Pedimento) {
|
||||
// Toggle: si ya está seleccionado, deseleccionar; si no, seleccionar
|
||||
@@ -114,6 +139,10 @@ let filters = $state({
|
||||
}
|
||||
|
||||
function handleEditSelected() {
|
||||
if (!canEditPedimento(get(currentUser))) {
|
||||
toast.error('No tiene permiso para editar pedimentos');
|
||||
return;
|
||||
}
|
||||
if (selectedId) {
|
||||
window.location.href = `/dashboard/pedimentos/edit/${selectedId}`;
|
||||
}
|
||||
@@ -123,6 +152,10 @@ let filters = $state({
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
if (!canDeletePedimento(get(currentUser))) {
|
||||
toast.error('No tiene permiso para eliminar pedimentos');
|
||||
return;
|
||||
}
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
@@ -268,18 +301,6 @@ let filters = $state({
|
||||
}
|
||||
}
|
||||
|
||||
useShortcuts(
|
||||
'Pedimentos',
|
||||
obtenerAtajosListaPedimento({
|
||||
manejarCrear: handleCreateClick,
|
||||
manejarActualizar: reloadData,
|
||||
manejarEditar: handleEditSelected,
|
||||
manejarEliminar: handleDelete,
|
||||
irATabla: focusFirstPedimentoTableRow,
|
||||
irAAcciones: focusPedimentoListFooterActions
|
||||
})
|
||||
);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
@@ -446,7 +467,10 @@ let filters = $state({
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
// Redirigir a la página de creación (reusa la página de edición con ID "new")
|
||||
if (!canOpenNuevoPedimentoForm(get(currentUser))) {
|
||||
toast.error('No tiene permiso para editar pedimentos');
|
||||
return;
|
||||
}
|
||||
window.location.href = '/dashboard/pedimentos/edit/new';
|
||||
}
|
||||
|
||||
@@ -455,6 +479,18 @@ let filters = $state({
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts(
|
||||
'Pedimentos',
|
||||
obtenerAtajosListaPedimento({
|
||||
manejarCrear: handleCreateClick,
|
||||
manejarActualizar: reloadData,
|
||||
manejarEditar: handleEditSelected,
|
||||
manejarEliminar: handleDelete,
|
||||
irATabla: focusFirstPedimentoTableRow,
|
||||
irAAcciones: focusPedimentoListFooterActions
|
||||
})
|
||||
);
|
||||
|
||||
// Opciones de status para el filtro
|
||||
const statusOptions = [
|
||||
{ value: '', label: 'Todos' },
|
||||
@@ -479,6 +515,13 @@ let filters = $state({
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
{#if !canViewList}
|
||||
<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}
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -500,7 +543,7 @@ let filters = $state({
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Button class="h-9" onclick={handleCreateClick}>
|
||||
<Button class="h-9" onclick={handleCreateClick} disabled={!canNuevoPedimentoAction}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nuevo Pedimento
|
||||
</Button>
|
||||
@@ -581,11 +624,21 @@ let filters = $state({
|
||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||
<!-- Botones de acción -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onclick={handleEditSelected} disabled={!hasSelection}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleEditSelected}
|
||||
disabled={!canEditSelected}
|
||||
>
|
||||
<Edit size={16} class="mr-1" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={handleDelete} disabled={!hasSelection}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleDelete}
|
||||
disabled={!canDeleteSelected}
|
||||
>
|
||||
<Trash2 size={16} class="mr-1" />
|
||||
Borrar
|
||||
</Button>
|
||||
@@ -593,7 +646,7 @@ let filters = $state({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleInterfaceAgenteAduanal}
|
||||
disabled={!hasSelection}
|
||||
disabled={!selectedPedimento}
|
||||
>
|
||||
<Send class="mr-2 h-4 w-4" />
|
||||
Interface Agente Aduanal
|
||||
@@ -602,6 +655,7 @@ let filters = $state({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Diálogo de confirmación para borrar -->
|
||||
<Dialog.Root bind:open={showDeleteDialog}>
|
||||
|
||||
@@ -29,7 +29,10 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Error fetching creation data:', response.status);
|
||||
// Graceful degradation: return empty data
|
||||
if (response.status === 403) {
|
||||
throw error(403, 'No tiene permiso para editar pedimentos');
|
||||
}
|
||||
// Graceful degradation: return empty data (no autorización / errores distintos de 403)
|
||||
return {
|
||||
pedimento: null,
|
||||
pedimentoId: null,
|
||||
@@ -102,6 +105,9 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
|
||||
if (response.status === 401) {
|
||||
throw redirect(302, '/login');
|
||||
}
|
||||
if (response.status === 403) {
|
||||
throw error(403, 'No tiene permiso para editar pedimentos');
|
||||
}
|
||||
throw error(response.status, 'Error al cargar el pedimento');
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +85,16 @@
|
||||
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import PrerequisitesModal from '$lib/components/dashboard/PrerequisitesModal.svelte';
|
||||
import { currentUser } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
import { canAccessPedimentoEditorPage } from '$lib/permissions/pedimento-permissions';
|
||||
|
||||
let { data }: { data: ExtendedPageData } = $props();
|
||||
|
||||
const canUsePedimentoEditor = $derived(
|
||||
canAccessPedimentoEditorPage($currentUser, !!data.isCreate)
|
||||
);
|
||||
|
||||
let activeTab = $state('general');
|
||||
|
||||
// Focus first input when switching main tabs (mouse or shortcut)
|
||||
@@ -1069,6 +1076,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !canUsePedimentoEditor}
|
||||
<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}
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -1297,3 +1311,4 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user