feat(roles): let each section decide who may assign work to others

Section access now only creates a record for yourself. Assigning it to a colleague requires a separate permission on that section.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-03 13:15:21 +03:30
co-authored by Cursor
parent 79733e6c96
commit 534dae45b2
25 changed files with 355 additions and 84 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ import type {
OrgUserDto,
} from '@mento/shared';
import { useAuthStore } from '@/stores/auth';
import { canReassignRecords } from '@/lib/roles';
import { canAssignToOthers } from '@/lib/assign-access';
import { activityTypeView, typeColorStyle, useActivityTypes } from '@/lib/activity-types';
import { brand } from '@mento/shared';
@@ -51,7 +51,7 @@ function goBack() {
}
const auth = useAuthStore();
const id = computed(() => (typeof route.params.id === 'string' ? route.params.id : ''));
const canReassign = computed(() => canReassignRecords(auth.user?.role));
const canReassign = computed(() => canAssignToOthers(auth.user, 'activities'));
const loading = ref(true);
const saving = ref(false);
+9 -3
View File
@@ -4,6 +4,7 @@ import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
import { canAssignToOthers } from '@/lib/assign-access';
import {
formatJalaliDayClock,
formatJalaliNumeric,
@@ -175,7 +176,10 @@ async function ensurePersonLoaded(pid: string) {
}
}
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'activities'));
function toggleAssigneeMenu() {
if (!canAssignOthers.value) return;
assigneeOpen.value = !assigneeOpen.value;
if (assigneeOpen.value) {
typeOpen.value = false;
@@ -1263,9 +1267,11 @@ watch([id, isNew], () => void load(), { immediate: true });
</div>
<small class="hint">
{{
teamGroups.length
? 'یک تیم یا نقش را انتخاب کنید تا همهٔ اعضایش مسئول شوند، یا چند نفر را جدا انتخاب کنید.'
: 'یک نقش را انتخاب کنید تا همهٔ اعضای آن مسئول شوند، یا چند نفر را جدا انتخاب کنید.'
canAssignOthers
? teamGroups.length
? 'یک تیم یا نقش را انتخاب کنید تا همهٔ اعضایش مسئول شوند، یا چند نفر را جدا انتخاب کنید.'
: 'یک نقش را انتخاب کنید تا همهٔ اعضای آن مسئول شوند، یا چند نفر را جدا انتخاب کنید.'
: 'بدون دسترسی «تخصیص فعالیت به دیگران» این فعالیت فقط برای خودتان ثبت می‌شود.'
}}
</small>
<div v-if="selectedRoles.length || selectedTeams.length || extraAssignees.length" class="assignee-chips">
+4 -1
View File
@@ -38,6 +38,7 @@ import {
} from '@ant-design/icons-vue';
import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
import { canAssignToOthers } from '@/lib/assign-access';
import { canUseBehavior } from '@/lib/behavior-access';
import { toFaDigits } from '@/lib/jalali';
import { currencySuffix, formatMoney } from '@/lib/money';
@@ -180,6 +181,7 @@ const personSelectOptions = computed(() =>
const companySelectOptions = computed(() =>
companies.value.map((c) => ({ value: c.id, label: c.name })),
);
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'sales'));
const ownerSelectOptions = computed(() =>
users.value.map((u) => ({ value: u.id, label: u.fullName })),
);
@@ -772,13 +774,14 @@ onMounted(load);
v-model:value="ownerId"
show-search
option-filter-prop="label"
:disabled="!canAssignOthers"
:options="ownerSelectOptions"
placeholder="انتخاب مسئول"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-col v-if="canAssignOthers" :xs="24" :md="12">
<a-form-item label="مسئولان مشترک">
<UserMultiSelect v-model="selectedAssigneeIds" :users="users" />
<a-typography-text type="secondary">علاوه بر مسئول اصلی.</a-typography-text>
+4 -4
View File
@@ -55,7 +55,7 @@ import {
toFaDigits,
} from '@/lib/jalali';
import { useAuthStore } from '@/stores/auth';
import { canManageSettings } from '@/lib/roles';
import { canAssignToOthers } from '@/lib/assign-access';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue';
import SmsComposeModal from '@/components/SmsComposeModal.vue';
@@ -82,7 +82,7 @@ type LeadDetail = LeadDto & { events: LeadEventDto[] };
const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const isAdmin = computed(() => canManageSettings(auth.user?.role));
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'leads'));
const canBehavior = computed(() => canUseBehavior(auth.user));
const copilotOpen = ref(false);
@@ -941,7 +941,7 @@ onBeforeUnmount(() => narrowMq?.removeEventListener('change', syncNarrow));
اعمال وضعیت
</a-button>
<template v-if="isAdmin">
<template v-if="canAssignOthers">
<a-divider orientation="right">تخصیص کارشناس</a-divider>
<a-form-item label="کارشناس">
<a-select
@@ -1085,7 +1085,7 @@ onBeforeUnmount(() => narrowMq?.removeEventListener('change', syncNarrow));
</a-form-item>
<a-button block :disabled="busy" @click="changeStage">اعمال وضعیت</a-button>
<template v-if="isAdmin">
<template v-if="canAssignOthers">
<a-divider orientation="right">تخصیص کارشناس</a-divider>
<a-form-item label="کارشناس">
<a-select
+7 -2
View File
@@ -58,6 +58,7 @@ import {
} from '@/lib/jalali';
import { useAuthStore } from '@/stores/auth';
import { canManageSettings } from '@/lib/roles';
import { canAssignToOthers } from '@/lib/assign-access';
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
@@ -147,6 +148,7 @@ const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const isAdmin = computed(() => canManageSettings(auth.user?.role));
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'leads'));
const q = ref(typeof route.query.q === 'string' ? route.query.q : '');
const loading = ref(false);
@@ -1355,7 +1357,7 @@ onBeforeUnmount(() => {
<a-typography-text>
{{ toFaDigits(selectedIds.length) }} مورد انتخاب‌شده
</a-typography-text>
<template v-if="isAdmin">
<template v-if="canAssignOthers">
<a-select
v-model:value="assignUserId"
placeholder="تخصیص به کارشناس"
@@ -1364,6 +1366,8 @@ onBeforeUnmount(() => {
:options="users.map((u) => ({ value: u.id, label: u.fullName }))"
/>
<a-button :disabled="!assignUserId || busy" @click="assignSelected">اعمال تخصیص</a-button>
</template>
<template v-if="isAdmin">
<a-select
v-model:value="bulkStageId"
placeholder="تغییر وضعیت"
@@ -1755,7 +1759,8 @@ onBeforeUnmount(() => {
<a-select
v-model:value="createForm.assigneeId"
allow-clear
placeholder="بدون مسئول"
:disabled="!canAssignOthers"
:placeholder="canAssignOthers ? 'بدون مسئول' : 'خودتان'"
style="width: 100%"
:options="users.map((u) => ({ value: u.id, label: u.fullName }))"
/>
+7 -2
View File
@@ -3,6 +3,8 @@ import { computed, onMounted, ref } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import { canAssignToOthers } from '@/lib/assign-access';
import { useAuthStore } from '@/stores/auth';
import type { OrgUserDto, ProjectCardDto } from '@mento/shared';
import UiIcon from '@/components/UiIcon.vue';
@@ -21,6 +23,8 @@ const emit = defineEmits<{
cancel: [];
}>();
const auth = useAuthStore();
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'projects'));
const route = useRoute();
const router = useRouter();
const id = computed(() => {
@@ -231,10 +235,11 @@ onMounted(async () => {
</div>
<div class="field">
<label for="card-assignees">مسئولان کارت</label>
<select id="card-assignees" v-model="selectedAssigneeIds" multiple class="multi">
<select id="card-assignees" v-model="selectedAssigneeIds" multiple class="multi" :disabled="!canAssignOthers">
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.fullName }}</option>
</select>
<small class="hint">با نگه داشتن Ctrl می‌توانید چند نفر را انتخاب کنید.</small>
<small v-if="canAssignOthers" class="hint">با نگه داشتن Ctrl می‌توانید چند نفر را انتخاب کنید.</small>
<small v-else class="hint">بدون دسترسی «تخصیص کارت پروژه به دیگران» کارت فقط برای خودتان ثبت می‌شود.</small>
</div>
<div class="field">
<label for="card-notes">یادداشت</label>
+7
View File
@@ -56,11 +56,18 @@ const router = useRouter();
/** مجوزی که ماژولش در سوپرادمین خاموش است در نقش‌ها نشان داده نمی‌شود. */
const PERMISSION_MODULES: Partial<Record<RolePermissionKey, OrgModuleKey[]>> = {
customers: ['customers'],
assign_customers: ['customers'],
leads: ['leads'],
assign_leads: ['leads'],
sales: ['pipeline'],
assign_sales: ['pipeline'],
billing: ['invoices', 'quotes'],
products: ['products'],
activities: ['activities'],
assign_activities: ['activities'],
assign_routines: ['routines'],
assign_projects: ['project_boards'],
assign_tickets: ['tickets'],
notes: ['notes'],
reports: ['reports'],
audit: ['audit_logs'],
+2 -2
View File
@@ -4,7 +4,7 @@ import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatJalaliDateTime } from '@/lib/jalali';
import { canViewAllRecords } from '@/lib/roles';
import { canAssignToOthers } from '@/lib/assign-access';
import { useAuthStore } from '@/stores/auth';
import {
UiBadge,
@@ -61,7 +61,7 @@ const faqHints = ref<FaqArticleDto[]>([]);
const csatScore = ref<number | null>(null);
const csatComment = ref('');
const canAssignOthers = computed(() => canViewAllRecords(auth.user?.role));
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'tickets'));
const showCsat = computed(() => {
if (!ticket.value || isNew.value) return false;