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:
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
TeamDto,
|
||||
TeamMemberRole,
|
||||
UpdatePriorArchiveCutoffRequest,
|
||||
UserTreasuryAccess,
|
||||
} from '@mento/shared';
|
||||
import {
|
||||
DATA_ACCESS_SECTION_LABELS,
|
||||
@@ -279,8 +280,13 @@ async function removeTeam(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const showTreasury = computed(() => {
|
||||
const modules = auth.user?.enabledModules;
|
||||
return !modules || modules.includes('treasury');
|
||||
});
|
||||
const expandedAccess = ref<string | null>(null);
|
||||
const draftAccess = ref<DataAccessPolicy>({});
|
||||
const draftTreasury = ref<UserTreasuryAccess | null>(null);
|
||||
const accessMode = ref<'inherit' | 'custom'>('inherit');
|
||||
const accessSaving = ref(false);
|
||||
|
||||
@@ -323,6 +329,7 @@ function openAccessEditor(u: OrgUserDto) {
|
||||
draftAccess.value = hasOverride
|
||||
? { ...clonePolicy(u.effectiveDataAccess), ...clonePolicy(override) }
|
||||
: clonePolicy(u.effectiveDataAccess);
|
||||
draftTreasury.value = u.treasuryAccessOverride ?? null;
|
||||
expandedAccess.value = u.id;
|
||||
}
|
||||
|
||||
@@ -333,8 +340,8 @@ async function saveUserAccess(userId: string) {
|
||||
try {
|
||||
const payload =
|
||||
accessMode.value === 'inherit'
|
||||
? { dataAccessOverride: null }
|
||||
: { dataAccessOverride: draftAccess.value };
|
||||
? { dataAccessOverride: null, treasuryAccessOverride: draftTreasury.value }
|
||||
: { dataAccessOverride: draftAccess.value, treasuryAccessOverride: draftTreasury.value };
|
||||
await api.patch(`/users/${userId}/data-access`, payload);
|
||||
ok.value = 'محدوده داده بهروز شد.';
|
||||
expandedAccess.value = null;
|
||||
@@ -528,8 +535,10 @@ onMounted(() => {
|
||||
سفارشی برای این کاربر
|
||||
</label>
|
||||
<DataAccessPolicyPanel
|
||||
v-if="accessMode === 'custom'"
|
||||
v-model="draftAccess"
|
||||
v-model:treasury-access="draftTreasury"
|
||||
:show-treasury="showTreasury"
|
||||
:data-locked="accessMode === 'inherit'"
|
||||
compact
|
||||
/>
|
||||
<div class="actions">
|
||||
|
||||
@@ -2,31 +2,30 @@
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Segmented as ASegmented } from 'ant-design-vue';
|
||||
import { treasurySectionAllowed, type TreasurySectionKey } from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const canPayroll = computed(
|
||||
() =>
|
||||
auth.user?.role === 'owner' ||
|
||||
auth.user?.role === 'admin' ||
|
||||
Boolean(auth.user?.permissions?.includes('treasury_payroll')),
|
||||
);
|
||||
|
||||
const links = computed(() => [
|
||||
{ value: '/finance', label: 'داشبورد', exact: true },
|
||||
{ value: '/finance/incomes', label: 'دریافتها و درآمدها' },
|
||||
{ value: '/finance/expenses', label: 'پرداختها و هزینهها' },
|
||||
{ value: '/finance/receivables', label: 'اقساط و مطالبات' },
|
||||
{ value: '/finance/accounts', label: 'صندوق و حسابها' },
|
||||
...(canPayroll.value ? [{ value: '/finance/payroll', label: 'حقوق' }] : []),
|
||||
{ value: '/finance/recurring', label: 'تکرارشونده' },
|
||||
{ value: '/finance/reports', label: 'گزارش' },
|
||||
{ value: '/finance/audit', label: 'سوابق' },
|
||||
{ value: '/finance/categories', label: 'دستهبندیها' },
|
||||
]);
|
||||
const links = computed(() => {
|
||||
const role = auth.user?.role;
|
||||
const permissions = auth.user?.permissions;
|
||||
const all: Array<{ value: string; label: string; exact?: boolean; section: TreasurySectionKey }> = [
|
||||
{ value: '/finance', label: 'داشبورد', exact: true, section: 'treasury_dashboard' },
|
||||
{ value: '/finance/incomes', label: 'دریافتها و درآمدها', section: 'treasury_incomes' },
|
||||
{ value: '/finance/expenses', label: 'پرداختها و هزینهها', section: 'treasury_expenses' },
|
||||
{ value: '/finance/receivables', label: 'اقساط و مطالبات', section: 'treasury_receivables' },
|
||||
{ value: '/finance/accounts', label: 'صندوق و حسابها', section: 'treasury_accounts' },
|
||||
{ value: '/finance/payroll', label: 'حقوق', section: 'treasury_payroll' },
|
||||
{ value: '/finance/recurring', label: 'تکرارشونده', section: 'treasury_recurring' },
|
||||
{ value: '/finance/reports', label: 'گزارش', section: 'treasury_reports' },
|
||||
{ value: '/finance/audit', label: 'سوابق', section: 'treasury_audit' },
|
||||
{ value: '/finance/categories', label: 'دستهبندیها', section: 'treasury_categories' },
|
||||
];
|
||||
return all.filter((link) => treasurySectionAllowed(role, permissions, link.section));
|
||||
});
|
||||
|
||||
const options = computed(() => links.value.map((l) => ({ value: l.value, label: l.label })));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user