feature/mercancias-permisos
This commit is contained in:
@@ -9,7 +9,7 @@ from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from core.database import get_core_db
|
||||
@@ -39,6 +39,50 @@ def _get_redis():
|
||||
return redis.Redis.from_url(url, decode_responses=False)
|
||||
|
||||
|
||||
def _assert_classes_csv_job_access(
|
||||
db: Session,
|
||||
job_id: str,
|
||||
current_user: Dict[str, Any],
|
||||
) -> None:
|
||||
from api.v1.modules.core.tasks_tracking.models import TaskRun
|
||||
|
||||
company_id: Optional[int] = None
|
||||
row = db.query(TaskRun).filter(TaskRun.task_id == job_id).first()
|
||||
if row is not None and row.company_id is not None:
|
||||
company_id = int(row.company_id)
|
||||
|
||||
r = _get_redis()
|
||||
if company_id is None:
|
||||
raw = r.get(f"{CLS_IMPORT_META_PREFIX}{job_id}")
|
||||
if raw:
|
||||
meta = json.loads(raw.decode("utf-8"))
|
||||
cid = meta.get("company_id")
|
||||
if cid is not None:
|
||||
company_id = int(cid)
|
||||
|
||||
if company_id is None and row is not None and row.meta_payload:
|
||||
layout_jid = row.meta_payload.get("layout_import_job_id")
|
||||
if layout_jid:
|
||||
raw2 = r.get(f"{CLS_IMPORT_META_PREFIX}{layout_jid}")
|
||||
if raw2:
|
||||
meta2 = json.loads(raw2.decode("utf-8"))
|
||||
cid2 = meta2.get("company_id")
|
||||
if cid2 is not None:
|
||||
company_id = int(cid2)
|
||||
|
||||
if company_id is None:
|
||||
raise HTTPException(status_code=403, detail="Sin acceso a este job")
|
||||
try:
|
||||
validate_access_to_resource(
|
||||
db, company_id, current_user, ["csv_upload.process"]
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Classes import: job access validation failed: %s", e)
|
||||
raise HTTPException(status_code=403, detail="Sin acceso a este job") from None
|
||||
|
||||
|
||||
@router.post("/upload", response_model=ImportJobResponse)
|
||||
async def upload_import_file(
|
||||
file: UploadFile = File(...),
|
||||
@@ -110,7 +154,12 @@ async def upload_import_file(
|
||||
|
||||
|
||||
@router.get("/{job_id}/status")
|
||||
async def get_import_status(job_id: str):
|
||||
async def get_import_status(
|
||||
job_id: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
_assert_classes_csv_job_access(db, job_id, current_user)
|
||||
task_result = celery_app.AsyncResult(job_id)
|
||||
|
||||
if task_result.state == "PENDING":
|
||||
@@ -192,6 +241,7 @@ async def commit_import_job(
|
||||
task_name="classes_insert_valid_rows",
|
||||
task_origin="a76/layouts_csv/classes/commit",
|
||||
args=[job_id],
|
||||
required_permissions=["csv_upload.process"],
|
||||
)
|
||||
return {
|
||||
"status": "committing",
|
||||
@@ -201,5 +251,10 @@ async def commit_import_job(
|
||||
|
||||
|
||||
@router.get("/{job_id}/errors/scan-csv")
|
||||
async def download_scan_errors_csv(job_id: str):
|
||||
async def download_scan_errors_csv(
|
||||
job_id: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
_assert_classes_csv_job_access(db, job_id, current_user)
|
||||
return download_scan_errors_csv_stream("cls", job_id)
|
||||
|
||||
@@ -29,6 +29,9 @@ def dispatch_tracked_layouts_csv_commit(
|
||||
"""
|
||||
Lee tenant_id / company_id del meta en Redis, valida acceso y despacha la tarea Celery
|
||||
con un task_id nuevo (commit) para que el polling use commit_job_id.
|
||||
|
||||
Si ``required_permissions`` es None, se normaliza a [] para validar acceso a compañía
|
||||
(``validate_access_to_resource(..., None)`` omitiría esa validación por el atajo tipo /me).
|
||||
"""
|
||||
raw = redis_client.get(meta_redis_key)
|
||||
if not raw:
|
||||
@@ -36,22 +39,29 @@ def dispatch_tracked_layouts_csv_commit(
|
||||
meta = json.loads(raw.decode("utf-8"))
|
||||
tenant_id = int(meta["tenant_id"])
|
||||
company_id = meta.get("company_id")
|
||||
if company_id is not None:
|
||||
try:
|
||||
validate_access_to_resource(
|
||||
db, int(company_id), current_user, required_permissions
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise HTTPException(status_code=403, detail="Sin acceso a este job") from None
|
||||
if company_id is None:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Job sin compañía asociada o meta incompleto",
|
||||
)
|
||||
effective_permissions: list[str] = (
|
||||
[] if required_permissions is None else required_permissions
|
||||
)
|
||||
try:
|
||||
validate_access_to_resource(
|
||||
db, int(company_id), current_user, effective_permissions
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise HTTPException(status_code=403, detail="Sin acceso a este job") from None
|
||||
|
||||
commit_id = str(uuid4())
|
||||
track_and_dispatch(
|
||||
db=db,
|
||||
task=celery_task,
|
||||
tenant_id=tenant_id,
|
||||
company_id=int(company_id) if company_id is not None else None,
|
||||
company_id=int(company_id),
|
||||
requested_by_user=current_user.get("preferred_username")
|
||||
or current_user.get("email")
|
||||
or current_user.get("sub"),
|
||||
|
||||
@@ -9,9 +9,9 @@ from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, Query, UploadFile, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from api.v1.modules.core.tasks_tracking import track_and_dispatch
|
||||
@@ -39,6 +39,50 @@ def _get_redis():
|
||||
return redis.Redis.from_url(url, decode_responses=False)
|
||||
|
||||
|
||||
def _assert_parts_csv_job_access(
|
||||
db: Session,
|
||||
job_id: str,
|
||||
current_user: Dict[str, Any],
|
||||
) -> None:
|
||||
from api.v1.modules.core.tasks_tracking.models import TaskRun
|
||||
|
||||
company_id: Optional[int] = None
|
||||
row = db.query(TaskRun).filter(TaskRun.task_id == job_id).first()
|
||||
if row is not None and row.company_id is not None:
|
||||
company_id = int(row.company_id)
|
||||
|
||||
r = _get_redis()
|
||||
if company_id is None:
|
||||
raw = r.get(f"{PART_IMPORT_META_PREFIX}{job_id}")
|
||||
if raw:
|
||||
meta = json.loads(raw.decode("utf-8"))
|
||||
cid = meta.get("company_id")
|
||||
if cid is not None:
|
||||
company_id = int(cid)
|
||||
|
||||
if company_id is None and row is not None and row.meta_payload:
|
||||
layout_jid = row.meta_payload.get("layout_import_job_id")
|
||||
if layout_jid:
|
||||
raw2 = r.get(f"{PART_IMPORT_META_PREFIX}{layout_jid}")
|
||||
if raw2:
|
||||
meta2 = json.loads(raw2.decode("utf-8"))
|
||||
cid2 = meta2.get("company_id")
|
||||
if cid2 is not None:
|
||||
company_id = int(cid2)
|
||||
|
||||
if company_id is None:
|
||||
raise HTTPException(status_code=403, detail="Sin acceso a este job")
|
||||
try:
|
||||
validate_access_to_resource(
|
||||
db, company_id, current_user, ["csv_upload.process"]
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Parts import: job access validation failed: %s", e)
|
||||
raise HTTPException(status_code=403, detail="Sin acceso a este job") from None
|
||||
|
||||
|
||||
@router.post("/upload", response_model=ImportJobResponse)
|
||||
async def upload_import_file(
|
||||
file: UploadFile = File(...),
|
||||
@@ -108,8 +152,12 @@ async def upload_import_file(
|
||||
|
||||
|
||||
@router.get("/{job_id}/status")
|
||||
async def get_import_status(job_id: str):
|
||||
from core.celery_app import celery_app
|
||||
async def get_import_status(
|
||||
job_id: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
_assert_parts_csv_job_access(db, job_id, current_user)
|
||||
task_result = celery_app.AsyncResult(job_id)
|
||||
|
||||
if task_result.state == "PENDING":
|
||||
@@ -191,6 +239,7 @@ async def commit_import_job(
|
||||
task_name="parts_insert_valid_rows",
|
||||
task_origin="a76/layouts_csv/parts/commit",
|
||||
args=[job_id],
|
||||
required_permissions=["csv_upload.process"],
|
||||
)
|
||||
return {
|
||||
"status": "committing",
|
||||
@@ -200,5 +249,10 @@ async def commit_import_job(
|
||||
|
||||
|
||||
@router.get("/{job_id}/errors/scan-csv")
|
||||
async def download_scan_errors_csv(job_id: str):
|
||||
async def download_scan_errors_csv(
|
||||
job_id: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
_assert_parts_csv_job_access(db, job_id, current_user)
|
||||
return download_scan_errors_csv_stream("part", job_id)
|
||||
|
||||
@@ -30,6 +30,7 @@ router.include_router(
|
||||
enable_filters=True,
|
||||
max_page_size=1000,
|
||||
list_permissions=["goods_parts.view"],
|
||||
get_permissions=["goods_parts.view"],
|
||||
create_permissions=["goods_parts.create"],
|
||||
update_permissions=["goods_parts.edit"],
|
||||
delete_permissions=["goods_parts.delete"],
|
||||
|
||||
@@ -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