fix(treasury): scope finance access and hide it when the module is off

Accounts can be edited, including the opening balance, and finance sections and accounts can be granted on a role or on one user. The finance menu stays hidden when the organization module is disabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 10:02:31 +03:30
co-authored by Cursor
parent 14f4e9d7d8
commit 1ccbefa7a3
26 changed files with 895 additions and 135 deletions
+109 -1
View File
@@ -424,6 +424,11 @@ export interface OrgUserDto {
* اگر کل آبجکت خالی باشد و نقش هم خالی باشد → بدون محدودیت عمقی.
*/
dataAccessOverride?: DataAccessPolicy;
/**
* null = دسترسی مالی از نقش می‌آید.
* آبجکت = جایگزین بخش‌ها و حساب‌های مالی همین کاربر.
*/
treasuryAccessOverride?: UserTreasuryAccess | null;
/** سیاست مؤثر پس از resolve (نقش + override) — فقط برای نمایش UI */
effectiveDataAccess?: DataAccessPolicy;
/** @deprecated از effectiveDataAccess.customers استفاده کنید */
@@ -596,6 +601,39 @@ export interface UpdateUserDataAccessRequest {
* آبجکت یعنی جایگزینی کامل override (کلید غایب در آبجکت = آن بخش از override پاک می‌شود).
*/
dataAccessOverride?: DataAccessPolicy | null;
/**
* null = دسترسی مالی از نقش.
* نبودن فیلد یعنی این ذخیره به دسترسی مالی دست نمی‌زند.
*/
treasuryAccessOverride?: UserTreasuryAccess | null;
}
/** دسترسی مالی اختصاصی یک کاربر؛ جایگزین نقش می‌شود */
export type UserTreasuryAccess = {
enabled: boolean;
sections: RolePermissionKey[];
ownAccounts: boolean;
accountIds: string[];
};
export function parseUserTreasuryAccess(raw: unknown): UserTreasuryAccess | null {
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
const obj = raw as Record<string, unknown>;
const sections = Array.isArray(obj.sections)
? obj.sections.filter(
(item): item is RolePermissionKey =>
typeof item === "string" && item.startsWith("treasury_"),
)
: [];
const accountIds = Array.isArray(obj.accountIds)
? obj.accountIds.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
: [];
return {
enabled: obj.enabled === true,
sections: [...new Set(sections)],
ownAccounts: obj.ownAccounts === true,
accountIds: [...new Set(accountIds.map((id) => id.trim()))],
};
}
export function emptySectionDataAccess(): SectionDataAccess {
@@ -741,7 +779,16 @@ export type RolePermissionKey =
| "conversations"
| "conversations_groups"
| "treasury"
| "treasury_dashboard"
| "treasury_incomes"
| "treasury_expenses"
| "treasury_receivables"
| "treasury_accounts"
| "treasury_payroll"
| "treasury_recurring"
| "treasury_reports"
| "treasury_audit"
| "treasury_categories"
| "behavior";
/** بخشی که مسئولش را می‌شود به کاربر دیگری داد */
@@ -787,10 +834,59 @@ export const ROLE_PERMISSION_ITEMS: Array<{
{ key: "conversations", label: "گفتگو" },
{ key: "conversations_groups", label: "ساخت گروه گفتگو" },
{ key: "treasury", label: "مدیریت مالی" },
{ key: "treasury_payroll", label: "حقوق و دستمزد" },
{ key: "treasury_dashboard", label: "مالی — داشبورد" },
{ key: "treasury_incomes", label: "مالی — دریافت‌ها و درآمدها" },
{ key: "treasury_expenses", label: "مالی — پرداخت‌ها و هزینه‌ها" },
{ key: "treasury_receivables", label: "مالی — اقساط و مطالبات" },
{ key: "treasury_accounts", label: "مالی — صندوق و حساب‌ها" },
{ key: "treasury_payroll", label: "مالی — حقوق و دستمزد" },
{ key: "treasury_recurring", label: "مالی — تکرارشونده" },
{ key: "treasury_reports", label: "مالی — گزارش" },
{ key: "treasury_audit", label: "مالی — سوابق" },
{ key: "treasury_categories", label: "مالی — دسته‌بندی‌ها" },
{ key: "behavior", label: "شخصیت‌شناسی و دستیار فروش" },
];
/** بخش‌های ریز مدیریت مالی. حقوق جداست و با تیک خودش باز می‌شود. */
export const TREASURY_SECTION_KEYS = [
"treasury_dashboard",
"treasury_incomes",
"treasury_expenses",
"treasury_receivables",
"treasury_accounts",
"treasury_payroll",
"treasury_recurring",
"treasury_reports",
"treasury_audit",
"treasury_categories",
] as const satisfies readonly RolePermissionKey[];
export type TreasurySectionKey = (typeof TREASURY_SECTION_KEYS)[number];
/**
* دسترسی کلی «مدیریت مالی» در را باز می‌کند.
* اگر هیچ بخش ریزی انتخاب نشده باشد همه بخش‌ها به‌جز حقوق بازند.
* با انتخاب حتی یک بخش ریز، فقط همان‌ها باز می‌مانند.
*/
export function treasurySectionAllowed(
role: string | null | undefined,
permissions: readonly string[] | null | undefined,
section: TreasurySectionKey | "treasury_documents",
): boolean {
if (role === "owner" || role === "admin") return true;
const perms = permissions ?? [];
if (section === "treasury_payroll") return perms.includes("treasury_payroll");
if (!perms.includes("treasury")) return false;
const fine = TREASURY_SECTION_KEYS.filter(
(key) => key !== "treasury_payroll" && perms.includes(key),
);
if (fine.length === 0) return true;
if (section === "treasury_documents") {
return perms.includes("treasury_incomes") || perms.includes("treasury_expenses");
}
return perms.includes(section);
}
export interface OrgRoleMemberDto {
id: string;
fullName: string;
@@ -816,6 +912,10 @@ export interface OrgRoleDto {
childCount: number;
userCount: number;
members: OrgRoleMemberDto[];
/** حساب‌های اضافی؛ خالی به‌تنهایی یعنی هنوز محدود نشده */
treasuryAccountIds: string[];
/** true یعنی اعضای نقش حساب‌هایی را می‌بینند که خودشان مسئول آن‌ها هستند */
treasuryOwnAccounts: boolean;
}
export interface CreateOrgRoleRequest {
@@ -826,6 +926,10 @@ export interface CreateOrgRoleRequest {
dataAccessPolicy?: DataAccessPolicy;
/** اگر ست شود، این نقش زیرمجموعهٔ نقش والد خواهد بود */
parentId?: string | null;
/** حساب‌های اضافی، جدا از حساب‌هایی که خودش مسئول است */
treasuryAccountIds?: string[];
/** دسترسی به حساب‌هایی که نام خود عضو در مسئول ثبت شده */
treasuryOwnAccounts?: boolean;
}
export interface UpdateOrgRoleRequest {
@@ -836,6 +940,10 @@ export interface UpdateOrgRoleRequest {
dataAccessPolicy?: DataAccessPolicy;
/** null یعنی جدا کردن از والد و تبدیل به ریشه */
parentId?: string | null;
/** حساب‌های اضافی، جدا از حساب‌هایی که خودش مسئول است */
treasuryAccountIds?: string[];
/** دسترسی به حساب‌هایی که نام خود عضو در مسئول ثبت شده */
treasuryOwnAccounts?: boolean;
}
export interface InviteUserRequest {