2026-10-22 11:53:26 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-10-25 19:22:30 +03:30
|
|
|
|
import { computed, watch } from 'vue';
|
2026-10-25 10:48:15 +03:30
|
|
|
|
import { Select as ASelect } from 'ant-design-vue';
|
2026-10-22 11:53:26 +03:30
|
|
|
|
import type {
|
|
|
|
|
|
DataAccessOwnerKind,
|
|
|
|
|
|
DataAccessRule,
|
|
|
|
|
|
DataAccessSection,
|
|
|
|
|
|
PersonMood,
|
|
|
|
|
|
SectionDataAccess,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
import {
|
|
|
|
|
|
DATA_ACCESS_SECTION_LABELS,
|
|
|
|
|
|
PERSON_IMPORT_SOURCES,
|
|
|
|
|
|
PERSON_MOOD_LABELS,
|
|
|
|
|
|
PERSON_MOODS,
|
|
|
|
|
|
sectionHasDataAccessLimits,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
export type IdOption = { id: string; name: string };
|
2026-10-25 19:22:30 +03:30
|
|
|
|
type Choice = { value: string; label: string };
|
2026-10-22 11:53:26 +03:30
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(
|
|
|
|
|
|
defineProps<{
|
|
|
|
|
|
modelValue: SectionDataAccess;
|
|
|
|
|
|
section?: DataAccessSection;
|
|
|
|
|
|
groups?: IdOption[];
|
|
|
|
|
|
stages?: IdOption[];
|
|
|
|
|
|
pipelines?: IdOption[];
|
|
|
|
|
|
boards?: IdOption[];
|
|
|
|
|
|
activityTypes?: IdOption[];
|
2026-10-25 19:22:30 +03:30
|
|
|
|
/** منبعهایی که در مشتریان همین محیط رکورد دارند */
|
|
|
|
|
|
importSources?: Choice[];
|
|
|
|
|
|
importSourcesReady?: boolean;
|
|
|
|
|
|
importSourcesFailed?: boolean;
|
2026-10-22 11:53:26 +03:30
|
|
|
|
disabled?: boolean;
|
|
|
|
|
|
compact?: boolean;
|
|
|
|
|
|
}>(),
|
|
|
|
|
|
{
|
|
|
|
|
|
section: 'customers',
|
|
|
|
|
|
groups: () => [],
|
|
|
|
|
|
stages: () => [],
|
|
|
|
|
|
pipelines: () => [],
|
|
|
|
|
|
boards: () => [],
|
|
|
|
|
|
activityTypes: () => [],
|
2026-10-25 19:22:30 +03:30
|
|
|
|
importSources: () => [],
|
|
|
|
|
|
importSourcesReady: false,
|
|
|
|
|
|
importSourcesFailed: false,
|
2026-10-22 11:53:26 +03:30
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:modelValue': [SectionDataAccess];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const sectionLabel = computed(() => DATA_ACCESS_SECTION_LABELS[props.section]);
|
|
|
|
|
|
|
|
|
|
|
|
const showGroups = computed(() => props.section === 'customers');
|
|
|
|
|
|
const showMoods = computed(() => props.section === 'customers');
|
|
|
|
|
|
const showImportSources = computed(() => props.section === 'customers');
|
|
|
|
|
|
const showLeadSources = computed(() => props.section === 'leads');
|
|
|
|
|
|
const showNoteTypes = computed(() => props.section === 'notes');
|
|
|
|
|
|
const showTicketStatuses = computed(() => props.section === 'tickets');
|
|
|
|
|
|
const showStages = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
props.section === 'leads' ||
|
|
|
|
|
|
props.section === 'sales' ||
|
|
|
|
|
|
props.section === 'activities' ||
|
|
|
|
|
|
props.section === 'projects',
|
|
|
|
|
|
);
|
|
|
|
|
|
const showPipelines = computed(() => props.section === 'sales');
|
|
|
|
|
|
const showBoards = computed(
|
|
|
|
|
|
() => props.section === 'activities' || props.section === 'projects',
|
|
|
|
|
|
);
|
|
|
|
|
|
const showTypes = computed(
|
|
|
|
|
|
() => props.section === 'activities' || props.section === 'routines',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const ownerSelfLabel = computed(() => {
|
|
|
|
|
|
if (
|
|
|
|
|
|
props.section === 'leads' ||
|
|
|
|
|
|
props.section === 'activities' ||
|
|
|
|
|
|
props.section === 'routines' ||
|
|
|
|
|
|
props.section === 'tickets'
|
|
|
|
|
|
) {
|
2026-10-22 13:13:59 +03:30
|
|
|
|
return 'مسئول خودش';
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
2026-10-22 13:13:59 +03:30
|
|
|
|
if (props.section === 'notes') return 'نویسنده خودش';
|
|
|
|
|
|
return 'پروندههای خود';
|
2026-10-22 11:53:26 +03:30
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const noteTypeOptions = [
|
|
|
|
|
|
{ value: 'general', label: 'عمومی' },
|
|
|
|
|
|
{ value: 'call_result', label: 'نتیجه تماس' },
|
|
|
|
|
|
{ value: 'meeting', label: 'جلسه' },
|
|
|
|
|
|
{ value: 'important', label: 'مهم' },
|
|
|
|
|
|
{ value: 'warning', label: 'هشدار' },
|
|
|
|
|
|
{ value: 'next_action', label: 'اقدام بعدی' },
|
|
|
|
|
|
{ value: 'follow_up', label: 'پیگیری' },
|
|
|
|
|
|
{ value: 'idea', label: 'ایده' },
|
|
|
|
|
|
{ value: 'suggestion', label: 'پیشنهاد' },
|
|
|
|
|
|
{ value: 'personal', label: 'شخصی' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const ticketStatusOptions = [
|
|
|
|
|
|
{ value: 'open', label: 'باز' },
|
|
|
|
|
|
{ value: 'in_progress', label: 'در حال انجام' },
|
|
|
|
|
|
{ value: 'waiting', label: 'در انتظار' },
|
|
|
|
|
|
{ value: 'resolved', label: 'حلشده' },
|
|
|
|
|
|
{ value: 'closed', label: 'بسته' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const typesLegend = computed(() =>
|
|
|
|
|
|
props.section === 'routines' ? 'دسته روتین' : 'نوع فعالیت',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-10-22 13:13:59 +03:30
|
|
|
|
const boardsLegend = computed(() =>
|
|
|
|
|
|
props.section === 'projects' ? 'بورد پروژه' : 'بورد فعالیت',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/** چند قانون قدیمی → یک فیلتر (اتحاد گزینهها) برای ویرایش ساده */
|
|
|
|
|
|
function collapseRules(rules: DataAccessRule[]): DataAccessRule {
|
|
|
|
|
|
if (!rules.length) return {};
|
|
|
|
|
|
if (rules.length === 1) return { ...rules[0]! };
|
|
|
|
|
|
const owners = new Set<DataAccessOwnerKind>();
|
|
|
|
|
|
const groupIds = new Set<string>();
|
|
|
|
|
|
const sources = new Set<string>();
|
|
|
|
|
|
const moods = new Set<PersonMood>();
|
|
|
|
|
|
const stageIds = new Set<string>();
|
|
|
|
|
|
const pipelineIds = new Set<string>();
|
|
|
|
|
|
const boardIds = new Set<string>();
|
|
|
|
|
|
const typeIds = new Set<string>();
|
|
|
|
|
|
for (const r of rules) {
|
|
|
|
|
|
for (const o of r.owners ?? []) owners.add(o);
|
|
|
|
|
|
for (const id of r.groupIds ?? []) groupIds.add(id);
|
|
|
|
|
|
for (const s of r.sources ?? []) sources.add(s);
|
|
|
|
|
|
for (const m of r.moods ?? []) moods.add(m);
|
|
|
|
|
|
for (const id of r.stageIds ?? []) stageIds.add(id);
|
|
|
|
|
|
for (const id of r.pipelineIds ?? []) pipelineIds.add(id);
|
|
|
|
|
|
for (const id of r.boardIds ?? []) boardIds.add(id);
|
|
|
|
|
|
for (const id of r.typeIds ?? []) typeIds.add(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
const out: DataAccessRule = {};
|
|
|
|
|
|
if (owners.size) out.owners = [...owners];
|
|
|
|
|
|
if (groupIds.size) out.groupIds = [...groupIds];
|
|
|
|
|
|
if (sources.size) out.sources = [...sources];
|
|
|
|
|
|
if (moods.size) out.moods = [...moods];
|
|
|
|
|
|
if (stageIds.size) out.stageIds = [...stageIds];
|
|
|
|
|
|
if (pipelineIds.size) out.pipelineIds = [...pipelineIds];
|
|
|
|
|
|
if (boardIds.size) out.boardIds = [...boardIds];
|
|
|
|
|
|
if (typeIds.size) out.typeIds = [...typeIds];
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ruleHasAnyFilter(rule: DataAccessRule): boolean {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
rule.owners?.length ||
|
|
|
|
|
|
rule.groupIds?.length ||
|
|
|
|
|
|
rule.sources?.length ||
|
|
|
|
|
|
rule.moods?.length ||
|
|
|
|
|
|
rule.stageIds?.length ||
|
|
|
|
|
|
rule.pipelineIds?.length ||
|
|
|
|
|
|
rule.boardIds?.length ||
|
|
|
|
|
|
rule.typeIds?.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hadMultipleRules = computed(
|
|
|
|
|
|
() => (props.modelValue?.rules?.length ?? 0) > 1,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-10-22 11:53:26 +03:30
|
|
|
|
const mode = computed({
|
|
|
|
|
|
get: () => (props.modelValue?.mode === 'rules' ? 'rules' : 'all'),
|
|
|
|
|
|
set: (next: 'all' | 'rules') => {
|
|
|
|
|
|
if (next === 'all') {
|
|
|
|
|
|
emit('update:modelValue', { mode: 'all', rules: [] });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-10-22 13:13:59 +03:30
|
|
|
|
const collapsed = collapseRules(props.modelValue?.rules ?? []);
|
|
|
|
|
|
emit('update:modelValue', {
|
|
|
|
|
|
mode: 'rules',
|
|
|
|
|
|
rules: [ruleHasAnyFilter(collapsed) ? collapsed : {}],
|
|
|
|
|
|
});
|
2026-10-22 11:53:26 +03:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-22 13:13:59 +03:30
|
|
|
|
const filter = computed(() => collapseRules(props.modelValue?.rules ?? []));
|
2026-10-22 11:53:26 +03:30
|
|
|
|
|
2026-10-22 13:13:59 +03:30
|
|
|
|
function emitFilter(next: DataAccessRule) {
|
|
|
|
|
|
const cleaned: DataAccessRule = {};
|
|
|
|
|
|
if (next.owners?.length) cleaned.owners = [...next.owners];
|
|
|
|
|
|
if (next.groupIds?.length) cleaned.groupIds = [...next.groupIds];
|
|
|
|
|
|
if (next.sources?.length) cleaned.sources = [...next.sources];
|
|
|
|
|
|
if (next.moods?.length) cleaned.moods = [...next.moods];
|
|
|
|
|
|
if (next.stageIds?.length) cleaned.stageIds = [...next.stageIds];
|
|
|
|
|
|
if (next.pipelineIds?.length) cleaned.pipelineIds = [...next.pipelineIds];
|
|
|
|
|
|
if (next.boardIds?.length) cleaned.boardIds = [...next.boardIds];
|
|
|
|
|
|
if (next.typeIds?.length) cleaned.typeIds = [...next.typeIds];
|
|
|
|
|
|
emit('update:modelValue', { mode: 'rules', rules: [cleaned] });
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 13:13:59 +03:30
|
|
|
|
function patchFilter(patch: Partial<DataAccessRule>) {
|
|
|
|
|
|
emitFilter({ ...filter.value, ...patch });
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 13:13:59 +03:30
|
|
|
|
function toggleOwner(kind: DataAccessOwnerKind) {
|
|
|
|
|
|
const current = new Set(filter.value.owners ?? []);
|
2026-10-22 11:53:26 +03:30
|
|
|
|
if (current.has(kind)) current.delete(kind);
|
|
|
|
|
|
else current.add(kind);
|
|
|
|
|
|
const owners = [...current] as DataAccessOwnerKind[];
|
2026-10-22 13:13:59 +03:30
|
|
|
|
patchFilter({ owners: owners.length ? owners : undefined });
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ListKey =
|
|
|
|
|
|
| 'groupIds'
|
|
|
|
|
|
| 'sources'
|
|
|
|
|
|
| 'moods'
|
|
|
|
|
|
| 'stageIds'
|
|
|
|
|
|
| 'pipelineIds'
|
|
|
|
|
|
| 'boardIds'
|
|
|
|
|
|
| 'typeIds';
|
|
|
|
|
|
|
2026-10-25 10:48:15 +03:30
|
|
|
|
type PickerMode = 'multiple' | 'tags';
|
|
|
|
|
|
type Picker = {
|
|
|
|
|
|
key: ListKey;
|
|
|
|
|
|
legend: string;
|
|
|
|
|
|
mode: PickerMode;
|
|
|
|
|
|
placeholder: string;
|
|
|
|
|
|
notFound: string;
|
|
|
|
|
|
hint?: string;
|
|
|
|
|
|
options: Choice[];
|
2026-10-25 19:22:30 +03:30
|
|
|
|
loading?: boolean;
|
2026-10-25 10:48:15 +03:30
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function namedOptions(items: IdOption[]): Choice[] {
|
|
|
|
|
|
return items.map((item) => ({ value: item.id, label: item.name }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function withSelected(options: Choice[], selected: readonly string[] | undefined): Choice[] {
|
|
|
|
|
|
const known = new Set(options.map((option) => option.value));
|
|
|
|
|
|
const extra = (selected ?? [])
|
|
|
|
|
|
.filter((id) => id && !known.has(id))
|
|
|
|
|
|
.map((id) => ({ value: id, label: id }));
|
|
|
|
|
|
return extra.length ? [...options, ...extra] : options;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function listValue(key: ListKey): string[] {
|
|
|
|
|
|
return ((filter.value[key] as string[] | undefined) ?? []).map(String);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-25 19:22:30 +03:30
|
|
|
|
const catalogSources = new Set<string>(PERSON_IMPORT_SOURCES);
|
|
|
|
|
|
|
|
|
|
|
|
/** کلیدهای ثابت کاتالوگ که در این محیط رکورد ندارند را نشان نده؛ متن سفارشی بماند. */
|
|
|
|
|
|
function sourcesInThisEnvironment(selected: readonly string[] | undefined): string[] {
|
|
|
|
|
|
const list = selected ?? [];
|
|
|
|
|
|
if (!props.importSourcesReady) {
|
|
|
|
|
|
return list.filter((source) => !catalogSources.has(source));
|
|
|
|
|
|
}
|
|
|
|
|
|
const present = new Set(props.importSources.map((option) => option.value));
|
|
|
|
|
|
return list.filter((source) => present.has(source) || !catalogSources.has(source));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const visibleImportSources = computed(() => sourcesInThisEnvironment(filter.value.sources));
|
|
|
|
|
|
|
2026-10-25 10:48:15 +03:30
|
|
|
|
function setList(key: ListKey, value: unknown) {
|
|
|
|
|
|
const raw = Array.isArray(value) ? value : value == null || value === '' ? [] : [value];
|
2026-10-25 19:22:30 +03:30
|
|
|
|
let list = [...new Set(raw.map((item) => String(item).trim()).filter(Boolean))];
|
|
|
|
|
|
if (key === 'sources' && showImportSources.value && props.importSourcesReady) {
|
|
|
|
|
|
list = sourcesInThisEnvironment(list);
|
|
|
|
|
|
}
|
2026-10-22 13:13:59 +03:30
|
|
|
|
patchFilter({ [key]: list.length ? list : undefined });
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-25 19:22:30 +03:30
|
|
|
|
function pickerValue(picker: Picker): string[] {
|
|
|
|
|
|
if (picker.key === 'sources' && showImportSources.value && props.importSourcesReady) {
|
|
|
|
|
|
return visibleImportSources.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return listValue(picker.key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(visibleImportSources, (next) => {
|
|
|
|
|
|
if (!showImportSources.value || !props.importSourcesReady) return;
|
|
|
|
|
|
const current = filter.value.sources ?? [];
|
|
|
|
|
|
if (current.length === next.length && current.every((source, index) => source === next[index])) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
patchFilter({ sources: next.length ? next : undefined });
|
|
|
|
|
|
});
|
2026-10-22 11:53:26 +03:30
|
|
|
|
|
2026-10-25 10:48:15 +03:30
|
|
|
|
const moodOptions: Choice[] = PERSON_MOODS.map((mood) => ({
|
|
|
|
|
|
value: mood,
|
|
|
|
|
|
label: PERSON_MOOD_LABELS[mood],
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const pickers = computed((): Picker[] => {
|
|
|
|
|
|
const rule = filter.value;
|
|
|
|
|
|
const items: Picker[] = [];
|
|
|
|
|
|
if (showGroups.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'groupIds',
|
|
|
|
|
|
legend: 'گروهها',
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: props.groups.length ? 'جستجو و انتخاب گروه…' : 'گروهی تعریف نشده',
|
|
|
|
|
|
notFound: props.groups.length ? 'موردی پیدا نشد' : 'گروهی تعریف نشده',
|
|
|
|
|
|
options: withSelected(namedOptions(props.groups), rule.groupIds),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showImportSources.value) {
|
2026-10-25 19:22:30 +03:30
|
|
|
|
const ready = props.importSourcesReady;
|
|
|
|
|
|
const failed = props.importSourcesFailed;
|
2026-10-25 10:48:15 +03:30
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'sources',
|
|
|
|
|
|
legend: 'منبع',
|
|
|
|
|
|
mode: 'tags',
|
2026-10-25 19:22:30 +03:30
|
|
|
|
placeholder: failed
|
|
|
|
|
|
? 'فهرست منبعها بارگذاری نشد'
|
|
|
|
|
|
: props.importSources.length
|
|
|
|
|
|
? 'جستجو، انتخاب یا نوشتن منبع جدید…'
|
|
|
|
|
|
: 'منبعی در این محیط نیست',
|
2026-10-25 10:48:15 +03:30
|
|
|
|
notFound: 'برای منبع جدید Enter بزنید',
|
2026-10-25 19:22:30 +03:30
|
|
|
|
hint: failed
|
|
|
|
|
|
? 'فهرست منبعهای این محیط خوانده نشد؛ انتخاب ذخیرهشده دست نخورده است.'
|
|
|
|
|
|
: 'فقط منبعهایی که در مشتریان این محیط هستند. نام تازه را بنویسید و Enter بزنید.',
|
|
|
|
|
|
loading: !ready && !failed,
|
|
|
|
|
|
options: withSelected(
|
|
|
|
|
|
ready ? props.importSources : [],
|
|
|
|
|
|
ready ? visibleImportSources.value : (rule.sources ?? []),
|
|
|
|
|
|
),
|
2026-10-25 10:48:15 +03:30
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showLeadSources.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'sources',
|
|
|
|
|
|
legend: 'منبع لید',
|
|
|
|
|
|
mode: 'tags',
|
|
|
|
|
|
placeholder: 'مثلاً اینستاگرام، سایت…',
|
|
|
|
|
|
notFound: 'نام را بنویسید و Enter بزنید',
|
|
|
|
|
|
hint: 'هر منبع را بنویسید و با Enter اضافه کنید.',
|
|
|
|
|
|
options: withSelected([], rule.sources),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showNoteTypes.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'sources',
|
|
|
|
|
|
legend: 'نوع یادداشت',
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: 'جستجو و انتخاب نوع…',
|
|
|
|
|
|
notFound: 'موردی پیدا نشد',
|
|
|
|
|
|
options: withSelected(noteTypeOptions, rule.sources),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showTicketStatuses.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'sources',
|
|
|
|
|
|
legend: 'وضعیت تیکت',
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: 'جستجو و انتخاب وضعیت…',
|
|
|
|
|
|
notFound: 'موردی پیدا نشد',
|
|
|
|
|
|
options: withSelected(ticketStatusOptions, rule.sources),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showMoods.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'moods',
|
|
|
|
|
|
legend: 'حال مشتری',
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: 'جستجو و انتخاب حال مشتری…',
|
|
|
|
|
|
notFound: 'موردی پیدا نشد',
|
|
|
|
|
|
options: withSelected(moodOptions, rule.moods),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showPipelines.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'pipelineIds',
|
|
|
|
|
|
legend: 'پایپلاین',
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: props.pipelines.length ? 'جستجو و انتخاب پایپلاین…' : 'پایپلاینی نیست',
|
|
|
|
|
|
notFound: props.pipelines.length ? 'موردی پیدا نشد' : 'پایپلاینی نیست',
|
|
|
|
|
|
options: withSelected(namedOptions(props.pipelines), rule.pipelineIds),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showStages.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'stageIds',
|
|
|
|
|
|
legend: 'مرحله',
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: props.stages.length ? 'جستجو و انتخاب مرحله…' : 'مرحلهای نیست',
|
|
|
|
|
|
notFound: props.stages.length ? 'موردی پیدا نشد' : 'مرحلهای نیست',
|
|
|
|
|
|
options: withSelected(namedOptions(props.stages), rule.stageIds),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showBoards.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'boardIds',
|
|
|
|
|
|
legend: boardsLegend.value,
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: props.boards.length ? 'جستجو و انتخاب…' : 'بوردی نیست',
|
|
|
|
|
|
notFound: props.boards.length ? 'موردی پیدا نشد' : 'بوردی نیست',
|
|
|
|
|
|
options: withSelected(namedOptions(props.boards), rule.boardIds),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showTypes.value) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: 'typeIds',
|
|
|
|
|
|
legend: typesLegend.value,
|
|
|
|
|
|
mode: 'multiple',
|
|
|
|
|
|
placeholder: props.activityTypes.length ? 'جستجو و انتخاب…' : 'موردی تعریف نشده',
|
|
|
|
|
|
notFound: props.activityTypes.length ? 'موردی پیدا نشد' : 'موردی تعریف نشده',
|
|
|
|
|
|
options: withSelected(namedOptions(props.activityTypes), rule.typeIds),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return items;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-22 11:53:26 +03:30
|
|
|
|
const hasLimits = computed(() => sectionHasDataAccessLimits(props.modelValue));
|
|
|
|
|
|
|
2026-10-22 13:13:59 +03:30
|
|
|
|
const filterSummary = computed(() => {
|
|
|
|
|
|
const rule = filter.value;
|
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
|
if (rule.owners?.includes('self')) parts.push(ownerSelfLabel.value);
|
|
|
|
|
|
if (rule.owners?.includes('team')) parts.push('تیمهای عضو');
|
|
|
|
|
|
if (rule.groupIds?.length) parts.push(`${rule.groupIds.length} گروه`);
|
|
|
|
|
|
if (rule.sources?.length) parts.push(`${rule.sources.length} مورد منبع/نوع`);
|
|
|
|
|
|
if (rule.moods?.length) parts.push(`${rule.moods.length} حال`);
|
|
|
|
|
|
if (rule.pipelineIds?.length) parts.push(`${rule.pipelineIds.length} پایپلاین`);
|
|
|
|
|
|
if (rule.stageIds?.length) parts.push(`${rule.stageIds.length} مرحله`);
|
|
|
|
|
|
if (rule.boardIds?.length) parts.push(`${rule.boardIds.length} بورد`);
|
|
|
|
|
|
if (rule.typeIds?.length) parts.push(`${rule.typeIds.length} نوع/دسته`);
|
|
|
|
|
|
return parts.length ? parts.join(' · ') : 'هنوز فیلتری انتخاب نشده';
|
|
|
|
|
|
});
|
2026-10-22 11:53:26 +03:30
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="dae" :class="{ compact }">
|
|
|
|
|
|
<div class="dae-mode">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<input v-model="mode" type="radio" value="all" :disabled="disabled" />
|
2026-10-22 13:13:59 +03:30
|
|
|
|
همهٔ {{ sectionLabel }}
|
2026-10-22 11:53:26 +03:30
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<input v-model="mode" type="radio" value="rules" :disabled="disabled" />
|
2026-10-22 13:13:59 +03:30
|
|
|
|
محدود با فیلتر
|
2026-10-22 11:53:26 +03:30
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-if="mode === 'rules'">
|
|
|
|
|
|
<p class="hint">
|
2026-10-22 13:13:59 +03:30
|
|
|
|
فقط رکوردهایی دیده میشوند که با
|
|
|
|
|
|
<strong>همهٔ فیلترهای پرشده</strong>
|
2026-10-25 10:48:15 +03:30
|
|
|
|
جور باشند. در هر فهرست، چند انتخاب یعنی هر کدام کافی است. خالی یعنی بدون محدودیت.
|
2026-10-22 11:53:26 +03:30
|
|
|
|
</p>
|
2026-10-22 13:13:59 +03:30
|
|
|
|
<p v-if="hadMultipleRules" class="hint warn">
|
|
|
|
|
|
تنظیم قبلی چندتکه بود؛ اینجا در یک فیلتر ادغام شده. با ذخیره به همین شکل ساده ثبت میشود.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="summary">{{ filterSummary }}</p>
|
|
|
|
|
|
|
|
|
|
|
|
<fieldset :disabled="disabled">
|
|
|
|
|
|
<legend>مالک / مسئول</legend>
|
|
|
|
|
|
<p class="muted">خالی = بدون محدودیت مالک</p>
|
|
|
|
|
|
<label class="chip">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="filter.owners?.includes('self')"
|
|
|
|
|
|
@change="toggleOwner('self')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{{ ownerSelfLabel }}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="chip">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="filter.owners?.includes('team')"
|
|
|
|
|
|
@change="toggleOwner('team')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
تیمهای عضو
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
|
2026-10-25 10:48:15 +03:30
|
|
|
|
<label v-for="picker in pickers" :key="picker.legend" class="pick">
|
|
|
|
|
|
<span>{{ picker.legend }}</span>
|
2026-10-25 19:22:30 +03:30
|
|
|
|
<p v-if="picker.loading" class="muted">در حال بارگذاری منبعها…</p>
|
2026-10-25 10:48:15 +03:30
|
|
|
|
<a-select
|
2026-10-25 19:22:30 +03:30
|
|
|
|
v-else
|
|
|
|
|
|
:value="pickerValue(picker)"
|
2026-10-25 10:48:15 +03:30
|
|
|
|
:mode="picker.mode"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
:options="picker.options"
|
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
|
:placeholder="picker.placeholder"
|
|
|
|
|
|
:not-found-content="picker.notFound"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="setList(picker.key, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p v-if="picker.hint" class="muted">{{ picker.hint }}</p>
|
|
|
|
|
|
</label>
|
2026-10-22 11:53:26 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<p v-else-if="!hasLimits" class="hint">
|
2026-10-22 13:13:59 +03:30
|
|
|
|
بدون محدودیت — همهٔ {{ sectionLabel }} قابل مشاهدهاند.
|
2026-10-22 11:53:26 +03:30
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.dae {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dae-mode {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.dae-mode label,
|
|
|
|
|
|
.chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
|
margin-inline-end: 0.55rem;
|
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.hint,
|
2026-10-22 13:13:59 +03:30
|
|
|
|
.muted,
|
|
|
|
|
|
.summary {
|
2026-10-22 11:53:26 +03:30
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
2026-10-22 13:13:59 +03:30
|
|
|
|
.hint.warn {
|
|
|
|
|
|
color: #b45309;
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
2026-10-22 13:13:59 +03:30
|
|
|
|
.summary {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #334155;
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
|
|
|
|
|
fieldset {
|
|
|
|
|
|
border: 1px dashed #e2e8f0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 0.5rem 0.65rem;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
legend {
|
|
|
|
|
|
padding-inline: 0.25rem;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
2026-10-25 10:48:15 +03:30
|
|
|
|
.pick {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
2026-10-25 10:48:15 +03:30
|
|
|
|
.pick > span {
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #475569;
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
2026-10-22 13:13:59 +03:30
|
|
|
|
.compact fieldset {
|
|
|
|
|
|
padding: 0.45rem 0.55rem;
|
2026-10-22 11:53:26 +03:30
|
|
|
|
}
|
|
|
|
|
|
</style>
|