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
@@ -3,13 +3,16 @@ import { computed, onMounted, ref, watch } from 'vue';
import type {
DataAccessPolicy,
DataAccessSection,
FinancialAccountDto,
PersonImportSourceStatDto,
RolePermissionKey,
SectionDataAccess,
UserTreasuryAccess,
} from '@mento/shared';
import {
DATA_ACCESS_SECTION_LABELS,
DATA_ACCESS_SECTIONS,
ROLE_PERMISSION_ITEMS,
emptySectionDataAccess,
} from '@mento/shared';
import { api } from '@/lib/api';
@@ -21,11 +24,16 @@ const props = defineProps<{
/** فقط بخش‌هایی که این مجوزها را دارند نشان بده؛ خالی = همه */
enabledPermissions?: RolePermissionKey[];
disabled?: boolean;
/** تب‌های عمق داده قفل شوند؛ تب مدیریت مالی جدا می‌ماند */
dataLocked?: boolean;
showTreasury?: boolean;
treasuryAccess?: UserTreasuryAccess | null;
compact?: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [DataAccessPolicy];
'update:treasuryAccess': [UserTreasuryAccess | null];
}>();
/** بخش‌هایی که به RolePermissionKey وصل‌اند؛ بقیه (روتین/پروژه/تیکت) همیشه دیده می‌شوند */
@@ -47,12 +55,13 @@ const visibleSections = computed(() => {
});
});
const active = ref<DataAccessSection>('customers');
const treasurySections = ROLE_PERMISSION_ITEMS.filter((item) => item.key.startsWith('treasury_'));
const active = ref<DataAccessSection | 'treasury'>('customers');
watch(
visibleSections,
(list) => {
if (!list.includes(active.value) && list[0]) active.value = list[0];
if (active.value !== 'treasury' && !list.includes(active.value) && list[0]) active.value = list[0];
},
{ immediate: true },
);
@@ -89,6 +98,48 @@ const activityTypes = ref<IdOption[]>([]);
const projectBoards = ref<IdOption[]>([]);
const projectStages = ref<IdOption[]>([]);
const routineCategories = ref<IdOption[]>([]);
const treasuryAccounts = ref<FinancialAccountDto[]>([]);
const treasuryAccountQuery = ref('');
const filteredTreasuryAccounts = computed(() => {
const query = treasuryAccountQuery.value.trim();
if (!query) return treasuryAccounts.value;
return treasuryAccounts.value.filter((account) => account.name.includes(query));
});
function emptyTreasuryAccess(): UserTreasuryAccess {
return { enabled: true, sections: [], ownAccounts: false, accountIds: [] };
}
function patchTreasury(next: UserTreasuryAccess | null) {
emit('update:treasuryAccess', next);
}
function toggleTreasurySection(key: RolePermissionKey) {
const current = props.treasuryAccess ?? emptyTreasuryAccess();
const sections = current.sections.includes(key)
? current.sections.filter((item) => item !== key)
: [...current.sections, key];
patchTreasury({ ...current, enabled: true, sections });
}
function toggleTreasuryAccount(id: string) {
const current = props.treasuryAccess ?? emptyTreasuryAccess();
const accountIds = current.accountIds.includes(id)
? current.accountIds.filter((item) => item !== id)
: [...current.accountIds, id];
patchTreasury({ ...current, enabled: true, accountIds });
}
async function loadTreasuryAccounts() {
if (!props.showTreasury) return;
try {
const { data } = await api.get<FinancialAccountDto[]>('/treasury/accounts');
treasuryAccounts.value = data;
} catch {
treasuryAccounts.value = [];
}
}
async function loadOptions() {
await loadCustomerGroups().catch(() => undefined);
@@ -176,8 +227,13 @@ async function loadOptions() {
onMounted(() => {
void loadOptions();
void loadTreasuryAccounts();
});
const editorSection = computed<DataAccessSection>(() =>
active.value === 'treasury' ? 'customers' : active.value,
);
const stagesForActive = computed(() => {
if (active.value === 'leads') return leadStages.value;
if (active.value === 'sales') return dealStages.value;
@@ -218,11 +274,94 @@ const typesForActive = computed(() => {
>
{{ DATA_ACCESS_SECTION_LABELS[s] }}
</button>
<button
v-if="showTreasury"
type="button"
class="tab"
:class="{ on: active === 'treasury' }"
role="tab"
:aria-selected="active === 'treasury'"
@click="active = 'treasury'"
>
مدیریت مالی
</button>
</div>
<div v-if="active === 'treasury'" class="treasury">
<p class="muted">
این برگه فقط برای همین کاربر است. «مثل نقش» یعنی همان دسترسی مالی نقش بماند.
در حالت سفارشی، اگر هیچ بخش ریزی تیک نخورد همه بخش‌ها به‌جز حقوق باز می‌مانند و حقوق جداست.
اگر هیچ حسابی انتخاب نشود همه حساب‌ها باز است.
</p>
<label class="choice">
<input
type="radio"
name="user-treasury"
:checked="treasuryAccess == null"
:disabled="disabled"
@change="patchTreasury(null)"
/>
مثل نقش
</label>
<label class="choice">
<input
type="radio"
name="user-treasury"
:checked="treasuryAccess != null"
:disabled="disabled"
@change="patchTreasury(treasuryAccess ?? emptyTreasuryAccess())"
/>
سفارشی برای این کاربر
</label>
<template v-if="treasuryAccess">
<label class="choice">
<input
type="checkbox"
:checked="treasuryAccess.enabled"
:disabled="disabled"
@change="patchTreasury({ ...treasuryAccess, enabled: !treasuryAccess.enabled })"
/>
دسترسی به مدیریت مالی
</label>
<template v-if="treasuryAccess.enabled">
<label v-for="perm in treasurySections" :key="perm.key" class="choice sub">
<input
type="checkbox"
:checked="treasuryAccess.sections.includes(perm.key)"
:disabled="disabled"
@change="toggleTreasurySection(perm.key)"
/>
{{ perm.label }}
</label>
<label class="choice">
<input
type="checkbox"
:checked="treasuryAccess.ownAccounts"
:disabled="disabled"
@change="patchTreasury({ ...treasuryAccess, ownAccounts: !treasuryAccess.ownAccounts })"
/>
حساب‌هایی که خودش مسئول آن‌هاست
</label>
<p class="muted">حساب‌های دیگر</p>
<input v-model="treasuryAccountQuery" placeholder="جستجوی حساب" :disabled="disabled" />
<label v-for="account in filteredTreasuryAccounts" :key="account.id" class="choice">
<input
type="checkbox"
:checked="treasuryAccess.accountIds.includes(account.id)"
:disabled="disabled"
@change="toggleTreasuryAccount(account.id)"
/>
{{ account.name }}
<template v-if="account.responsibleName"> — مسئول: {{ account.responsibleName }}</template>
</label>
<p v-if="!treasuryAccounts.length" class="muted">حسابی برای انتخاب نیست.</p>
</template>
</template>
</div>
<DataAccessEditor
v-else
:key="active"
:section="active"
:model-value="sectionValue(active)"
:section="editorSection"
:model-value="sectionValue(editorSection)"
:groups="customerGroups"
:stages="stagesForActive"
:pipelines="pipelines"
@@ -231,9 +370,9 @@ const typesForActive = computed(() => {
:import-sources="importSources"
:import-sources-ready="importSourcesReady"
:import-sources-failed="importSourcesFailed"
:disabled="disabled"
:disabled="disabled || dataLocked"
:compact="compact"
@update:model-value="setSection(active, $event)"
@update:model-value="setSection(editorSection, $event)"
/>
</template>
</div>
@@ -268,4 +407,17 @@ const typesForActive = computed(() => {
color: #64748b;
font-size: 0.85rem;
}
.treasury {
display: grid;
gap: 0.45rem;
}
.choice {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.4rem;
align-items: center;
}
.choice.sub {
padding-inline-start: 1.2rem;
}
</style>