panels-origin/web-panel/composables/useDesktop.ts
Cursor Agent aed2f4531b
Implement client IAM v2: roles per tenant, CRUD matrix, scope, and panel UI
- Add IAM v2 auth with role_id, is_owner, status checks, and API key without tenant_admin bypass
- Enforce CRUD permissions and project/warehouse scope across API routes
- Rewrite usuarios.vue with ribbon, tree, user/role management, and permission matrix
- Add dynamic menu, route guards, usePermissions/useScope composables
- Apply role templates on create; filter project docs by category; hide cost tab without permission
- Add iam_test unit tests; update bootstrap and SaaS tenant admin to use role_id

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:16:59 +00:00

178 lines
4.7 KiB
TypeScript

import type { TreeNode } from "primevue/treenode";
export type RibbonItem = {
key: string;
label: string;
icon: string;
disabled?: boolean;
title?: string;
command?: () => void;
};
export type RibbonGroup = {
key: string;
label: string;
items: RibbonItem[];
};
export type DeskDetail = {
kind: "none" | "worker" | "project" | "budget" | "iam-user" | "iam-role";
id?: number;
doc?: string;
tab?: string;
project?: Record<string, unknown>;
budgetItem?: Record<string, unknown>;
iamUser?: Record<string, unknown>;
iamRole?: Record<string, unknown>;
};
export type DeskStatusMessage = {
key: string;
severity: "danger" | "warn" | "info";
title: string;
detail?: string;
route?: string;
filter?: string;
treeKey?: string;
};
export type PadronViewFilter =
| "all"
| "with_imss"
| "without_imss"
| "in_project_without_imss"
| "imss_ready"
| "incomplete"
| "ready_gafete"
| "unassigned"
| "freshness";
/** @deprecated alias */
export type PadronImssFilter = PadronViewFilter;
export function useDesktop() {
const tabLabel = useState("desk-tab", () => "Inicio");
const ribbon = useState<RibbonGroup[]>("desk-ribbon", () => []);
const tree = useState<TreeNode[]>("desk-tree", () => []);
const treeKey = useState("desk-tree-key", () => "");
const treeTitle = useState("desk-tree-title", () => "Módulo");
const showTree = useState("desk-show-tree", () => true);
const showDetail = useState("desk-show-detail", () => true);
const detail = useState<DeskDetail>("desk-detail", () => ({ kind: "none" }));
const countLabel = useState("desk-count", () => "");
const detailRev = useState("desk-rev", () => 0);
const globalQ = useState("desk-global-q", () => "");
const onTreeSelect = useState<((key: string) => void) | null>("desk-on-tree", () => null);
const statusMessages = useState<DeskStatusMessage[]>("desk-status-messages", () => []);
const padronImssFilter = useState<PadronViewFilter>("padron-imss-filter", () => "all");
function setStatusMessages(messages: DeskStatusMessage[]) {
statusMessages.value = messages;
}
function clearStatusMessages() {
statusMessages.value = [];
}
function configure(opts: {
tab: string;
treeTitle?: string;
ribbon?: RibbonGroup[];
tree?: TreeNode[];
treeKey?: string;
showTree?: boolean;
showDetail?: boolean;
countLabel?: string;
onTreeSelect?: (key: string) => void;
}) {
tabLabel.value = opts.tab;
if (opts.treeTitle != null) treeTitle.value = opts.treeTitle;
if (opts.ribbon) ribbon.value = opts.ribbon;
if (opts.tree) tree.value = opts.tree;
if (opts.treeKey != null) treeKey.value = opts.treeKey;
if (opts.showTree != null) showTree.value = opts.showTree;
if (opts.showDetail != null) showDetail.value = opts.showDetail;
if (opts.countLabel != null) countLabel.value = opts.countLabel;
onTreeSelect.value = opts.onTreeSelect ?? null;
}
function selectTree(key: string) {
treeKey.value = key;
onTreeSelect.value?.(key);
}
function openWorker(id: number, doc?: string, tab?: string) {
detail.value = { kind: "worker", id, doc, tab };
showDetail.value = true;
}
function openProject(row: Record<string, unknown> & { id: number }, doc?: string) {
detail.value = { kind: "project", id: row.id, project: row, doc };
showDetail.value = true;
}
function openBudgetItem(item: Record<string, unknown> & { id: number }) {
detail.value = { kind: "budget", id: item.id, budgetItem: item };
showDetail.value = true;
}
function openIamUser(row: Record<string, unknown> & { id: number }) {
detail.value = { kind: "iam-user", id: row.id, iamUser: row };
showDetail.value = true;
}
function openIamRole(row: Record<string, unknown> & { id: number }) {
detail.value = { kind: "iam-role", id: row.id, iamRole: row };
showDetail.value = true;
}
function clearDetail() {
detail.value = { kind: "none" };
}
function bumpDetail() {
detailRev.value += 1;
}
const loanPrompt = useState<{ visible: boolean; workerId?: number; personName?: string }>("desk-loan", () => ({
visible: false,
}));
function openLoan(workerId: number, personName?: string) {
loanPrompt.value = { visible: true, workerId, personName };
}
function closeLoan() {
loanPrompt.value = { visible: false };
}
return {
tabLabel,
ribbon,
tree,
treeKey,
treeTitle,
showTree,
showDetail,
detail,
countLabel,
detailRev,
globalQ,
statusMessages,
padronImssFilter,
setStatusMessages,
clearStatusMessages,
configure,
selectTree,
openWorker,
openProject,
openBudgetItem,
openIamUser,
openIamRole,
clearDetail,
bumpDetail,
loanPrompt,
openLoan,
closeLoan,
};
}